Gateway and Realtime Transport¶
Every fact the ONIX web client shows comes from one of two backend gateway processes, reached exclusively through a single same-origin Next.js API route. This page describes that transport, its error and reconnect behavior, the trust boundary that gates writes, and how the frontend pins the backend contract it accepts. It intentionally says nothing about credentials, tokens, or internal hostnames.
One proxy, two backend surfaces¶
src/app/api/engine/[...path]/route.ts is a Next.js route handler that forwards allowlisted requests to one of two backend gateway processes, each resolved server-side from its own environment variable (ONIX_ENGINE_GATEWAY_URL for the v1 fixture/runtime gateway, ONIX_SPINE_GATEWAY_URL for the spine.v2 gateway) — never from a browser-supplied value. The route's own documentation gives its reason for existing plainly: the backend "binds loopback only and publishes no CORS contract, so the browser cannot reach it directly." The proxy is stated to be deliberately thin — "it forwards allowlisted requests and relays the body verbatim," holds no fixtures, and has "no fallback path": if the backend is down, the proxy produces a failure the UI renders as unavailable rather than making an absent backend look present.
Two surfaces share this one route:
- The v1 fixture/runtime gateway (
src/gateway/endpoints.ts) — GET-only, publishing collections like/bars,/signals,/retirements,/screener-rows,/chart-markers, plus single documents (/backtest-result,/provenance) and six named SSE streams under/stream/<name>. - The
spine.v2gateway (src/gateway/v2-endpoints.ts) — GET plus a closed, enumerated set of POST command routes (backtests, sandbox start/stop, screener screen submission, and a provisional connection/execution command set) — explicitly "a closed command surface, not a general write path."
Two transports, deliberately different shapes¶
v1 documents are schema-tagged, cursor-sequenced JSON, and its SSE frames (id: <sequence>\ndata: <json>\n\n) carry a resumable ?cursor= query parameter. src/gateway/sse.ts implements this over raw fetch and a manual ReadableStream reader rather than the browser's built-in EventSource, and gives a specific reason: EventSource cannot set request headers, so resuming via Last-Event-ID would require a second, header-based resume mechanism layered on top of the ?cursor= parameter a first connection still needs — "two resume mechanisms doing the same job." EventSource's automatic reconnect is also unbounded and uncancellable, which conflicts with this module's bounded, cancellable backoff. A pure core (parseSseFrame, decideFrame, computeReconnectDelayMs) turns one frame plus the current cursor into a decision with no I/O, so reconnect/resync/duplicate/stale-frame logic is unit-testable without a network.
spine.v2 (src/gateway/v2.ts) is a different shape entirely: every response body must declare version: "spine.v2"; errors arrive as {version, error: {code, detail}}; its streams deliver named events (bar / quote / trade / heartbeat) with no resume cursor at all. The low-level SSE framing and reconnect backoff are shared with the v1 client (imported from sse.ts) so the two surfaces cannot drift on transport mechanics, even though their document and error shapes differ.
Error vocabulary and capability gating¶
Every gateway read or write resolves to a typed GatewayFailure with one of a closed set of kinds — TRANSPORT, UPSTREAM_ERROR, MALFORMED_RESPONSE, CONTRACT_VIOLATION — carrying the true upstream HTTP status when the proxy has it, never conflating "the browser could not reach the proxy" with "the backend answered with an error." Before a poll or subscription is opened at all, many surfaces first consult the backend's own published capability registry (GET /v2/registry, read by src/gateway/capabilities.ts), whose seven-value status vocabulary — READY, DISABLED, UNSUPPORTED, UNAVAILABLE, UNKNOWN, WARMING, UNPUBLISHED — is treated as seven genuinely different claims rather than a single collapsed "on/off." The module's own comment stresses that "transport reachability lives in gateway.unreachable and is never a capability claim," and that this registry "gates compile-time-known surfaces only": a capability the frontend has no code for is never conjured from registry data alone.
The trust boundary: deployment mode, not user identity¶
No user login, session, or account system was found in the frontend source at this pin (see evidence/frontend/open-questions.md). The actual trust boundary the frontend enforces is a two-valued, server-side, host-level setting: src/gateway/deployment-mode.ts's ONIX_DEPLOYMENT_MODE, resolved to either private-operator or public-readonly. The module's own documentation states the design intent precisely: this is "a property of the HOST, not of the browser that connected to it," read only from the server environment, "never inferred from a request, a header, a cookie, or a client-supplied value" — because "a mode the caller could influence would be a mode the caller could choose." It fails closed: any unset, misspelled, or unrecognized value resolves to public-readonly, the restrictive option, deliberately the opposite of how the module handling gateway URLs behaves (that one throws loudly on a bad value, because a wrong URL silently accepted would present the wrong backend as the configured one).
mutationsAllowed(mode) is the single predicate every private route is gated behind; a public-readonly host refuses every one of the closed POST command routes in the proxy itself, before reading a body or resolving an origin. A second layer (src/gateway/public-edge.ts) withholds even attempting those requests client-side once the host has stated public-readonly (or before it has stated anything at all) — the module's own reasoning is that a poll refused every ten seconds is still an unwanted poll: "a Manager list on an interval, a job stream reconnecting on backoff, a sandbox status tick" each become "a steady stream of 403s from a browser that had already been told the answer." This is stated to be honesty, not enforcement: the proxy remains the actual boundary.
The accepted backend contract pin¶
The frontend does not vendor contract schemas from a mutable working tree. scripts/engine-pin.mjs names two immutable commit/tree pairs read exclusively through git show <commit>:<path> against the separate onix-backend repository: an accepted release pair (the functional source and handoff commits the deployed artifact was built from) and a contract authority pair (the source and handoff commits that own the vendored schema bytes) — the module's own comment explains why these are kept as two separate facts rather than one: "vendored contract authority and deployable release identity are different facts and conflating them is how a pin rots." At this pin, both pairs' contract-schema set agrees: 58 schemas, content hash 28fdebfb5b5a318455dacdff4375deb77237dc5a1d56ab26238cd27f59c2ec42. Every vendored schema file's own byte-for-byte digest is checked against the pin before use (verifiedBlob), and the generated manifest that lists them is checked against an out-of-band digest held in this same file — specifically so that "a tampered manifest — even one whose internal digests are all consistent with tampered files — fails here." See evidence/frontend/contract-pin.md for the full mechanics and what this means for a reader auditing the pin.
Status¶
The proxy, both transport shapes, the typed failure vocabulary, the capability-registry gate, and the deployment-mode trust boundary are verified against source and unit tests at this pin (route-v2.test.ts, route-deployment-mode.test.ts, sse.test.ts, v2.test.ts, capabilities.test.ts, public-edge coverage under f9-acceptance.test.tsx). The absence of a user-identity/auth system is stated as unverified in the strict sense that a negative cannot be exhaustively proven by search — see evidence/frontend/open-questions.md for exactly what was checked. See ../backend/gateway-schemas.md for the backend's own account of the gateway contract it publishes.
Evidence and source pins for this page
Verified. Current behaviour, confirmed in source at the pinned commit.
Verified on against the following immutable sources:
frontend@99f49dae:src/app/api/engine/[...path]/route.tsfrontend@99f49dae:src/gateway/deployment-mode.tsfrontend@99f49dae:src/gateway/sse.tsfrontend@99f49dae:src/gateway/v2.tsfrontend@99f49dae:scripts/engine-pin.mjsfrontend@99f49dae:src/gateway/capabilities.ts
Status tokens are defined on the documentation and status model page. Every pin on this site is listed under versions and source pins.