How the pieces fit together and talk. For what each component is see the top-level README and the per-component READMEs; this is the data flow and the parts that aren't obvious from the component list.
node ──subxt──▶ chain-indexer ──writes──▶ DB ◀─reads/writes─▶ indexer-api ──GraphQL──▶ clients / wallets
│ ▲
└──event (IDs)──▶ NATS ──▶ wallet-indexer ──writes relevant txs──┘
- chain-indexer is the single writer. It subscribes to the node over subxt (a
finalized-block subscription), applies each block to its own
LedgerState- recomputing and guarding the merkle roots - and writes blocks, transactions and ledger state to the DB. Only one may run per environment (two would race the DB). It publishes small indexing events (BlockIndexed,UnshieldedUtxoIndexed). - wallet-indexer does the per-wallet work asynchronously in the background - the
least-obvious component. It subscribes to
BlockIndexed(the new-data signal) and polls the active wallet set (active_wallet_ids), trial-decrypts each new transaction against each active wallet's viewing key, materialises the relevant transactions into the DB, and emitsWalletIndexed. - indexer-api serves GraphQL queries and subscriptions (reads) and owns the wallet-lifecycle
writes - it is read-heavy, not read-only.
connectupserts the wallet into thewalletstable (the encrypted viewing key, a freshsession_id, and the scan start index) and returns the session ID;disconnectnulls the session; and the shielded subscription periodically writes akeep_wallet_activeheartbeat. A newly connected wallet is picked up by wallet-indexer polling the active wallet set, not via a connect event; subscriptions then stream that wallet's relevant transactions. - spo-indexer indexes stake-pool data via Blockfrost.
NATS carries small event messages - IDs only (block ID, transaction ID, wallet); the data itself stays in the DB. So the queue stays light regardless of chain size. Ledger state used to live in NATS and was moved to the DB (more reliable across abrupt restarts) - NATS remains for messaging only.
Deployed clusters run a NATS quorum of 3 and typically 2 wallet-indexer replicas for redundancy, alongside the single chain-indexer and the HPA'd indexer-api.
- cloud - the four services (chain-indexer, indexer-api, wallet-indexer, spo-indexer) + PostgreSQL + NATS, as separate images. This is what runs in Kubernetes.
- standalone - one
indexer-standalonebinary with SQLite and an in-memory pub/sub in place of NATS. For local dev / single-operator use.
The messaging seam is indexer-common's pub_sub (NATS for cloud, in-memory channels for
standalone); the SQL migrations also live in indexer-common/migrations.
- Testing & node consistency - the runtime root-match guard.
- Per-component detail: chain-indexer, wallet-indexer, indexer-api.