BFF Boundary
gordon-manager is the Backend-for-Frontend (BFF). It is the single token surface between the browser and the rest of the system. This page defines which traffic goes through the manager, which goes direct, and why.
The rule
Stateful reads — REST snapshots, projections, historical replay — go through gordon-manager. One token surface, one ACL.
Stateful writes — operator commands (POST /bff/risk/resume, POST /bff/risk/pause/:bot_id, POST /bff/emergency-flatten) — go through gordon-manager. Manager validates the browser-supplied operator token, proxies to the destination service with a separately-held forward token, and owns the manager-side audit log. One token surface for browser writes; one canonical audit trail.
Stateless realtime NATS subjects — risk.events.>, market.klines.>, trading.fills.{bot_id} — the browser subscribes direct via NATS-WS using a manager-issued JWT. Manager does not proxy these. Stateless means no replay requirement, no historical-correctness requirement, and no value-add from a proxy hop.
What this prevents
Two failure modes:
- Second-token surfaces. If gordon-console holds a token for gordon-risk directly, there are two places where browser auth lives. A token rotation, a revocation, or an audit query must cover both. The BFF rule eliminates the second surface.
- Service-to-console direct write paths. A service that writes directly to the browser (outside the manager fan-out) bypasses the manager's rate limiting, auth check, and audit log.
Traffic map
Current drift (DP-12)
gordon-console currently calls POST /risk/resume directly on gordon-risk via an internal riskWriterClient. The manager-proxied endpoint POST /bff/risk/resume is designed but not yet wired.
This is documented as an active story (DP-12 + DP-13) in plan/active/. The drift is declared, not hidden. Until DP-12 ships:
- gordon-console holds a direct token for gordon-risk for the resume action only.
- All other write paths already go through manager.
- The audit trail for resume actions is incomplete (manager does not log what it did not proxy).
The fix is to wire POST /bff/risk/resume in gordon-manager, update gordon-console to call the BFF endpoint, and retire riskWriterClient entirely.
Case convention at the edge
The manager is the boundary between two naming worlds:
| Context | Convention | Reason |
|---|---|---|
| Browser-bound JSON (REST responses, WS messages, NATS-WS egress) | camelCase | JS/TS convention |
Bus payloads on NATS, Rust-to-Rust HTTP, sqlx FromRow | snake_case | Rust serde default + Postgres convention |
Conversion happens at the edge, never in the bus and never in the producer. Today: gordon-manager's WS broadcaster does the snake_case-to-camelCase conversion for the five DirectFromNotify channels (risk_halt_changed, breaker_state, portfolio_state, source_freshness, backfill_jobs) before fanning out to the console.
Rust services keep emitting snake_case on the bus. Console reducers always receive camelCase. The edge converter is the only place that knows both vocabularies. Adding snake_case keys in a REST response to the browser is a boundary violation.
Invariants
- gordon-manager is the only service that holds a browser-facing operator token surface for write actions.
- No service writes directly to the browser outside the manager fan-out (except stateless NATS-WS with a manager-issued JWT).
- snake_case-to-camelCase conversion happens at the manager edge, never in upstream producers.
- A REST response to the browser that contains snake_case keys is a defect — it means the conversion was omitted.
- DP-12 drift is declared and tracked. Until it ships, the resume path is the only violating surface.
Related
- Architecture — full service topology and token model.
- Event Flow — NATS subject topology and manager's
manager-riskconsumer. - Risk Management —
/risk/resume,/risk/emergency-flattenendpoints.