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:4000Minimum config:
| Var | Effect if unset |
|---|---|
DATABASE_URL | Reads return empty lists; /readyz reports degraded |
ENGINE_BASE_URL | Defaults to http://localhost:8080 |
ENGINE_ADMIN_TOKEN | SSE 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 byapp.authenticate+app.requireOwnParty(both no-ops whileAUTH_DISABLED=true). - Pagination is by
seqorbefore, 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
| Family | Source | Pages |
|---|---|---|
| Vault, collateral, batches | On-chain projection | REST |
| Orders, fills, positions, triggers | Engine mirror | REST |
| Verifications | Cross-check engine ↔ chain | REST |
| Trading | Signed proxy to engine | Authentication |
| Live | WebSocket fan-out | WebSocket |
| System | Health, metrics, docs | Operations |