Titandocs

Overview

A Fastify BFF that reads the indexer's Postgres, proxies signed trading calls, and fans one SSE firehose out to every browser.

The BFF (api/) is the single entry point for the front end. Two dependencies behind it:

  • Postgres (the indexer's, read-only Drizzle) — on-chain projection plus the engine mirror.
  • Engine REST (non-custodial proxy) — signed order placement, session auth, orderbook, user snapshot.
Frontend ──REST + WS──► BFF ──read SQL──► Postgres  ◄──writes── Indexer
                         │                                          ▲
                         ├──signed proxy──► Engine /v1/orders       │
                         └──SSE (bearer)──► Engine admin firehose ──┘

Quick start

cd api
cp .env.example .env
npm install
npm run dev            # http://localhost:4000

Minimum config:

VarEffect if unset
DATABASE_URLReads return empty lists; /readyz reports degraded
ENGINE_BASE_URLDefaults to http://localhost:8080
ENGINE_ADMIN_TOKENSSE bridge does not start — WS clients connect and receive nothing
curl http://localhost:4000/healthz
curl http://localhost:4000/readyz
curl http://localhost:4000/v1/vault
curl 'http://localhost:4000/v1/orderbook?depth=10'

OpenAPI / Swagger UI: http://localhost:4000/docs

Conventions

All decimals are strings. Ten decimal places, mirroring Canton. Never parse a balance or size into a JS number for arithmetic — only at the formatting boundary.

  • Routes under /v1/users/:party/... are guarded by app.authenticate + app.requireOwnParty (both no-ops while AUTH_DISABLED=true).
  • Pagination is by seq or before, never by offset.
  • Public reads take ?market=; scoped reads take it too, plus the party in the path.

Deploy order

Indexer first, then api. Always.

The BFF reads 11 TP/SL v2 columns (engine_orders.reduce_only, tp_*, sl_*, engine_triggers.size, limit_price, fired_size, fired_limit) that only exist after indexer migrations 0014_tpsl_v2 and 0015_fired_limit. Drizzle emits an explicit column list, so deploying the api first makes /v1/orders and /v1/users/:party/triggers fail immediately with column "size" does not exist.

Engine version floor: server c879f7f (2026-08-01).

Live open-order reads call GET /v1/admin/users/{party}/orders?market=all. The reserved value all only exists from that commit, which also made ?market= mandatory (400 market_required). Against an older engine, all is a 400 market_unsupported: the call is caught, the live half disappears, and /v1/users/:party/orders falls back to non-terminal mirror rows — reintroducing exactly the phantom open orders this read exists to eliminate.

Silent failure, no 5xx. Watch for ordersForUser: live open-orders fetch failed in the logs.

Route families

FamilySourcePages
Vault, collateral, batchesOn-chain projectionREST
Orders, fills, positions, triggersEngine mirrorREST
VerificationsCross-check engine ↔ chainREST
TradingSigned proxy to engineAuthentication
LiveWebSocket fan-outWebSocket
SystemHealth, metrics, docsOperations

On this page