Indexer
The only writer to Postgres — two input streams, one schema, and a migration ordering rule that breaks deploys.
server/indexer — Go. Consumes the Canton ledger stream and the engine's signed WAL, and
projects both into Postgres.
This is the deployed tree. The standalone titan/indexer at the repository root is stale.
Two inputs, one schema
Canton ledger (gRPC stream) ──┐
├──► Postgres
Engine WAL (firehose) ───┘| Source | Tables |
|---|---|
| Ledger | vaults, deposit_receipts, withdrawal_requests, withdrawal_counters, batch_manifests, user_batches |
| Engine WAL | engine_orders, engine_fills, engine_triggers, engine_positions, engine_events |
The engine_* tables are the mirror: an off-chain projection that lets the BFF answer history
queries without asking the engine.
Migrations
Numbered and applied in order. Two matter for the BFF contract:
| Migration | Adds |
|---|---|
0014_tpsl_v2 | engine_orders.reduce_only, tp_*, sl_*; engine_triggers.size, limit_price |
0015_fired_limit | engine_triggers.fired_size, fired_limit |
Migrate the indexer before deploying the api. The BFF's Drizzle queries emit an explicit
column list, so an api deployed ahead of these migrations fails on the first request with
column "size" does not exist — not gradually, immediately.
Setup
gRPC stubs
Generated once from proto/ into internal/ledger/pb/. Commit them on your branch if you vendor
them.
make protoConfigure
config.yaml plus .env. See .env.example.
Run
make runIts own REST + WebSocket
The indexer exposes read endpoints and a WebSocket of its own. The interface does not use them — it goes through the BFF, which aggregates and adds auth. The indexer's API is for tooling, debugging, and operational queries.
Two WebSocket endpoints in one stack is a recurring source of confusion. The one browsers connect
to is the BFF's /v1/stream. The indexer's is not exposed publicly.
Build tags
The indexer uses Go build tags to select ledger backends and optional features. make targets
wrap the right combinations — prefer them over hand-rolled go build invocations, which is also
where the multi-package -o trap from the engine page applies.
End-to-end verification
The repository ships an E2E check that drives activity through the engine and asserts the projection matches. It is the fastest way to confirm a schema change did not silently drop a field — a nullable column that never gets written looks identical to a healthy one at the type level.
Lag
The BFF exposes bff_engine_max_seq. Comparing it against the indexer's high-water mark gives
indexer lag directly.
Live WebSocket frames bypass the indexer entirely (they come from SSE), so during lag live data is correct while REST reads are stale. A fresh order book beside an empty position list is this, not a UI bug.