BackJanuary 2026

Indus

Indus Hero Image

Tools

  • React 19
  • Rails 8
  • Rust
  • PostgreSQL
  • Kafka
  • Temporal
  • Gemini
  • AWS EKS
  • Terraform

Overview

Indus is a financial intelligence platform. It streams live market data across stocks and crypto, puts fifty-plus metrics on every company, and explains each one in the context of the company you are actually looking at. It runs on a distributed platform: a Rust ingestion service, a Rails 8 API, and Temporal workflows, deployed to AWS.

Team

The Problem

Every finance app on the market assumes you already know what you are reading. You open a company page and get P/E, EV/EBITDA, free cash flow yield, and forty more figures stacked in a grid. The data is all there. Whether any of it is normal for this company, in this industry, in this quarter, is not.

So you open a second tab, search the term, and get a textbook definition written about no company in particular. By the time you come back you have lost the thread. That gap between having the number and understanding the number is where almost everyone researching a company actually gets stuck, and it is the gap Indus was built to close.

The apps that skip the gap go straight to a verdict instead. Buy, sell, a score out of ten. Those answer a different question, and you cannot learn anything from them.

The Rule

Indus explains, and it never advises. It will tell you what debt-to-equity measures and why this company’s looks high against its sector. It will not tell you whether to buy.

That is the honest position when a model is doing the talking, and it is also the more useful one. A recommendation ends the conversation. An explanation means you can read the next company yourself. It also made the product decisions easy: anything that looked like a verdict was out, which is why there is no score, no rating, and no portfolio tracker anywhere in the app.

The Company Page

Price and chart at the top, then every metric below. Fifty-plus of them, covering valuation ratios, margins, growth rates, and financial health indicators, with cryptocurrencies sitting alongside equities on the same surface.

Charts run on TradingView’s Lightweight Charts, so the price view is the one traders already know and it stays readable panning across years of history. Nothing is hidden behind progressive disclosure. Showing a beginner five metrics and burying the rest decides for them what they are allowed to be curious about, and the explanations are what keep the full grid from being intimidating.

The Indus company page for NVIDIA: price and change, headline metrics, a one-year candlestick chart with volume, and the full grid of financial metrics.
Figure 1. Company page: live pricing, chart, and the full metrics grid.

Explanations

Hover any metric and an explanation arrives, written about the company you are on rather than the concept in the abstract. It streams token by token, so text is on screen immediately instead of after a spinner, and it never takes you off the page.

If that is not enough, the same hover opens into a chat scoped to that one metric. It already knows which number you are asking about, so you can just ask: why is this high, is that normal for this industry, what would move it. Scoping the chat to a single metric was deliberate. An open-ended assistant sitting on a finance app gets asked what to buy, and answering that is the one thing Indus will not do.

Gemini sits behind a provider-neutral model gateway on the server, so the product is never coupled to one vendor’s API and prompts, quotas, and keys stay off the client entirely.

A hover card explaining NVIDIA's 74.7% gross margin, beside the Indus Analyst chat scoped to Gross Margin answering why it is higher than other chip companies.
Figure 2. Hover explanation expanding into a chat scoped to that metric.

Discovery

Universal search covers every publicly traded company and cryptocurrency in one box, because that is how people actually hold things now rather than in two separate mental buckets.

If you do not arrive with a ticker, there is sector browsing across Tech, Finance, Healthcare, and Energy, plus trending names. Most beginners are interested in an industry long before they are interested in a company, and the app meets them there.

The Indus Discover page with symbol search and companies grouped by category, starting with Tech Giants and Financial Services.
Figure 3. Search and sector browsing.

Favorites and Reports

Favorites persist across devices behind Amazon Cognito sign-in, with SRP authentication and Cognito-only access-token verification at the Rails boundary. Come back next week on a different machine and the companies you were working through are still there.

Reports pull what you have been reading into a generated artifact you can keep. They are produced by a durable workflow rather than a request handler, validated against the evidence that went into them, and written to S3. It is the closest the product comes to an opinion, and it stays on the right side of the line by summarising what is true about a company rather than what to do about it.

A row of favorited companies with prices and sparklines, above a generated NVIDIA research report.
Figure 4. Saved companies and a generated report.

Architecture

Indus is not a single application. It is four services with real boundaries between them: a React 19 front end on Vite with TanStack Router and Query, a Ruby 3.4 and Rails 8 API with Pundit authorisation and Sidekiq for background work, a Rust market-data service under services/market-data, and a Temporal-backed workflow tier for research and report generation.

Those boundaries are enforced rather than assumed. Service contracts are defined in OpenAPI and Protobuf, and clients are generated deterministically from them, so a breaking change to an API surfaces at build time instead of in production. The whole topology runs locally through Docker Compose with the same service boundaries as the deployed system, so nobody needs Ruby, Rust, PostgreSQL, Kafka, and Temporal installed on their laptop to work on one piece of it.

In production it sits on AWS EKS with Aurora PostgreSQL, ElastiCache, MSK, S3, and Secrets Manager, described in Terraform and Helm and deployed by Argo CD through GitOps. The earlier Next.js and Supabase implementation is still maintained as a controlled rollback path, with a documented procedure covering traffic, workloads, data reconciliation, and evidence retention.

Market Data

Market data is its own service, written in Rust on Tokio and Axum. It ingests from Alpaca, publishes onto Kafka topics market.bars.v1 and market.quotes.v1, and persists to PostgreSQL under an explicit retention policy, which keeps the ingestion path off the API’s request path entirely.

Every event on the bus carries the same Protobuf envelope. That envelope is the reason the system can be reasoned about at all: correlation_id and causation_id let a report be traced back through the workflow to the tick that triggered it, and idempotency_key is what makes a redelivered message safe to process twice.

pub const SCHEMA_VERSION: u32 = 1;
pub const BARS_TOPIC: &str = "market.bars.v1";
pub const QUOTES_TOPIC: &str = "market.quotes.v1";

#[derive(Clone, PartialEq, Message)]
pub struct EventEnvelope {
	#[prost(string, tag = "1")]
	pub event_id: String,
	#[prost(uint32, tag = "2")]
	pub schema_version: u32,
	#[prost(string, tag = "3")]
	pub event_type: String,
	#[prost(string, tag = "6")]
	pub correlation_id: String,
	#[prost(string, tag = "7")]
	pub causation_id: String,
	#[prost(string, tag = "8")]
	pub idempotency_key: String,
}

services/market-data/src/event.rs — the envelope every market event carries.

Prices reach the browser over authenticated server-sent events, so a client only ever receives the stream it is entitled to and the connection survives the long idle stretches that kill naive polling. Choosing Rust here was about the shape of the workload rather than taste: ingestion is a hot loop over a firehose, and it should not be contending with a request-response API for the same runtime.

Research Workflows

Report generation is long-running, involves several model calls, and absolutely cannot half-finish. So it does not live in a controller. Work is handed off through a transactional outbox, which means the database commit and the message that triggers the work succeed or fail together and a crash between them cannot silently drop a job.

The publisher drains unpublished rows in batches, validates each envelope before it goes near the bus, and stamps published_at only once the broker has accepted it. Failures are not retried in a tight loop either — each attempt backs off exponentially, capped by MAX_BACKOFF, so a provider having a bad afternoon does not turn into a self-inflicted thundering herd.

def publish_one(event_id)
	event = OutboxEvent.find(event_id)
	return false if event.published_at?

	Events::Envelope.validate!(event.payload)
	envelope = event.payload.fetch("envelope")
	@producer.publish(topic: event.topic, key: event.aggregate_id, payload: event.payload,
		headers: { "event_id" => envelope.fetch("event_id"),
			"correlation_id" => envelope.fetch("correlation_id") })
	event.update!(published_at: @clock.current, attempts: event.attempts + 1)
	true
rescue StandardError => error
	record_failure(event, error) if event
	false
end

def record_failure(event, error)
	attempts = event.attempts + 1
	delay = [ 2**[ attempts, 8 ].min, MAX_BACKOFF ].min
	event.update_columns(attempts: attempts, next_attempt_at: @clock.current + delay.seconds)
end

apps/platform-api/app/services/events/outbox_publisher.rb — publish, then record the failure with backoff.

From there Kafka delivers to Temporal, which executes the workflow with retries and durable state, validates the evidence behind each claim, and writes the finished artifact to S3. A report either exists and is backed by evidence, or it does not exist. There is no third state where a user is holding a half-generated document.

Reliability

Every upstream provider is treated as something that will eventually fail. Calls run under explicit deadlines with retries, caching, and defined fallbacks, behind rate limits, with health checks and diagnostics in front of them, all documented rather than folded into whoever wrote the call site.

Verification is layered to match. RSpec, RuboCop, and Brakeman on the Rails side, tests and Clippy on the Rust side, Vitest on the web, and Playwright with axe-core across desktop and mobile. The suite covers pgTAP database security tests that replay migrations against row-level security policies, auth-boundary integration tests, WCAG A/AA accessibility checks, and production-mode performance budgets. Accessibility and security are part of the test run, not a pass somebody remembers to do later.

Design

Finance interfaces are usually dense and alarm-coloured, which is the wrong mood for a tool about taking your time. The surface stays quiet and lets the numbers carry it, with red and green reserved for actual price movement instead of sprayed across the whole interface.

Light and dark are both first-class rather than one being an afterthought with a filter over it, and the accessibility checks in CI mean contrast and focus order stay correct as the product grows.

Decisions

  • No portfolio tracking

    The obvious next feature, left out on purpose. Once the app knows what you own, every explanation reads as advice about your money, and the one rule the product is built on gets much harder to hold.

  • No price alerts

    Alerts are what make a finance app sticky, and also what turn a research tool into something that pings you about a three percent move. That is precisely the thing Indus was built as an alternative to, so the engagement lost is the point rather than a regression.

  • Nothing here is proprietary

    All of it is public somewhere. The value is that it is attached to the number that confused you, at the moment it confused you, and cutting that second tab mattered more than any individual feature.

What’s Next

Comparison is next. The questions people ask after the first few are comparative: this company against its competitors, or against its own history. Right now that means two tabs, which is the exact thing the product set out to remove.

After that, richer reports, and some way for the explanations someone reads to accumulate over time instead of being answered and forgotten. The platform is already shaped for it: the workflow tier exists, the evidence validation exists, and the model gateway means adding reasoning is a service change rather than a rewrite.