Overview
The four services, what each one is authoritative for, and the single direction data flows between them.
Titan splits into five processes, each authoritative for exactly one thing. When two components disagree, the table below decides who wins.
| Component | Language | Authoritative for | Never does |
|---|---|---|---|
| Daml contracts | Daml | Collateral custody, vault state, batch manifests | Matching |
| Engine | Go | Order matching, positions, funding, liquidation | Custody |
| Indexer | Go | Projecting ledger + engine WAL into Postgres | Writing to the engine |
BFF (api/) | TypeScript | Read aggregation, WS fan-out, signature pass-through | Holding keys |
| Interface | React | Presentation, signing UX | Deriving truth |
Wiring
┌──────────────┐
REST + WS │ Interface │
┌──────────────────┤ React 19 │
│ └──────────────┘
▼
┌────────────────┐ read SQL ┌──────────────────┐
│ BFF (Fastify) ├────────────►│ Postgres │
│ /v1 │ │ (indexer-owned) │
└───┬────────┬───┘ └────────▲─────────┘
│ │ │ writes only
│ │ SSE (bearer ADMIN_TOKEN) │
│ └──────────────┐ ┌────────┴─────────┐
│ signed REST proxy │ │ Indexer (Go) │
▼ ▼ └────────▲─────────┘
┌────────────────────────────────┐ │ signed WAL firehose
│ Engine (Go) — matching ├──────┘
│ /v1/orders /v1/session-auth │
└───────────────┬────────────────┘
│ merkle roots, batch manifests
▼
┌────────────────┐ ┌──────────────────┐
│ Canton ledger │◄───────┤ Oracle (index px)│
│ Daml contracts │ └──────────────────┘
└────────────────┘Postgres has exactly one writer. The indexer writes; the BFF reads with a read-only Drizzle
client. This is why deploy order matters: indexer first, then api. The BFF's Drizzle queries
emit explicit column lists, so shipping an api that expects columns a migration has not created
yet fails immediately with column "…" does not exist.
Why a BFF at all
The engine speaks a signature-oriented, market-scoped API and the indexer speaks SQL. Neither is a good browser API. The BFF exists to:
Aggregate reads. One GET /v1/users/:party/positions instead of a join the browser has to
write itself.
Fan out the firehose. The engine exposes a single admin SSE stream gated by a bearer token that must never reach a browser. The BFF holds one SSE connection and multiplexes it to every WebSocket client, filtered per subscription.
Recompose domain frames. Four in-process projections turn raw WAL events into things a UI can
render directly — position.update, balance.update, orderbook.snapshot, candle.update.
Pass signatures through untouched. X-User, X-Session-Id, X-Session-Sig are forwarded
verbatim. The BFF cannot forge them, so a compromised BFF cannot trade.
Degraded modes
The stack is designed to fail partially rather than totally.
| Missing | Effect |
|---|---|
DATABASE_URL | Reads return empty lists; /readyz reports it. The UI renders empty, not broken. |
ENGINE_ADMIN_TOKEN | SSE bridge does not start. WS clients connect and receive nothing. |
| Engine down | Reads still serve from Postgres. Placement 502s. |
| Indexer lagging | Reads serve stale data; /readyz exposes bff_engine_max_seq for the gap. |
A staging incident worth remembering: a duplicate party hint made the engine return 502, and Cloudflare replaced the response with its own error page — which carries no CORS headers. The browser therefore reported a CORS failure for what was really a 502. Always check the origin status before believing a CORS error.