Console
Sign in
Browse docs

Decisions

Maintainer documentation — you don't need this to use or operate the platform.

The decision log behind the gateway contract: each question, the decision taken, where the spec states it and the evidence that proves it. It is for maintainers; spec.md is normative for behavior, and the table format and the rule for contract changes are in platform decisions.

Read byRepository maintainer

Code cites these entries as "decision N"; numbers never change. A "Stated in" of — means the decision shapes the implementation without a dedicated spec section.

#QuestionDecisionStated inStatusEvidence
1Supported runtime versionsBuilt, tested and released on Bun 1.4.3; the contract says Bun ≥ 1.4.3. Bun 1.2.0 and 1.2.23 lack the early answer, body-producer cancellation, observable upstream handshake refusal and the caller upgrade with gateway headers.§4.2resolvedinternals.md, test/network/transport.test.ts
2Upload and response commitment boundariesObservable events are the end of the request stream, the final response headers and the first body byte; peer acceptance is not claimed. A failure after headers ends the body instead of replacing the answer.—resolvedtest/network/transport.test.ts
3Refusal of an upstream WebSocket handshakeDistinct from a network failure: the caller gets 502 bad_gateway without the upstream body, the status is logged and the slot released.—resolvedtest/network/transport.test.ts
4Connection establishment during an uploadNo extra ceiling: the upload is bounded only by the idle bodyMs, and connecting counts toward responseMs.—resolvedtest/network/transport.test.ts
5The caller's Authorization header and the standard Forwarded headerforwarded is removed both ways; Authorization is forwarded unchanged, since identity travels in the signed gateway headers.§7.1resolvedsrc/domains/forwarding/headers.test.ts
6Header representationRoles are comma-separated; a role with a comma or control character is dropped from the header but sent to Access. Identity values are cut to 512 characters and stripped of control characters. Cookie names are case-sensitive.—resolvedsrc/domains/forwarding/identity-signature.test.ts, src/domains/forwarding/headers.test.ts
7Token purposeThe provider has no token-type claim, so the gateway checks signature (pinned algorithms), issuer, audience, expiry (five seconds' tolerance) and subject. Token type is decision 38.—resolvedtest/integration/oidc.test.ts
8Consuming the login transactionSingle-use four ways: the state cookie is deleted, the callback state must match it, verifier and nonce bind the exchange, and the provider's code is single-use.—resolvedsrc/domains/authentication/principal.test.ts
9Decision-cache outageOnly explicit decisions are cached. Without the cache the gateway asks again and denies on failure.—resolvedsrc/domains/workspace-access/workspace-access.test.ts, test/integration/access.test.ts
10Live WebSocket authorizationChecked at the handshake and every GATEWAY_WS_REVALIDATE_INTERVAL_MS (route, session, fresh access decision); any failure, or a check that cannot run, closes with 1008.§10resolvedsrc/domains/websockets/bridge.test.ts, test/contract/websocket-revocation.test.ts
11Provisioning the signature verifierUpstreams got a derived key, versioned v3. Superseded by decisions 40 and 57.§7.4supersededsrc/domains/forwarding/identity-signature.test.ts
12External host and response cachingNo response caching and no added cache headers; upstream responses pass as is.—resolvedtest/contract/endpoints.test.ts
13Replica topology and shared-state TLSReplicas share sessions, limits and decisions through a server verified with the deployment CA. Process caps are per process, so replicas multiply them.—resolvedoperations.md, src/runtime/env/
14Drain policyReadiness answers 503 draining, the listener stops, WebSockets close with 1001, and exchanges are cancelled at the deadline.—resolvedtest/contract/lifecycle.test.ts
15Request hostThe effective host is a trusted proxy's x-forwarded-host (last value), else Host; untrusted or implausible values are ignored. It picks the route, is checked against the origin and is signed.§7.3resolvedtest/contract/forwarding.test.ts
16Request metricsEvery answer is counted, refusals included, by route or unmatched. Labels are bounded (other; overflow beyond 500 routes).§12.1resolvedtest/contract/metrics.test.ts, src/observability/observability.test.ts
17Limit scopesFour: deployment, caller across routes, caller per route, subject per route. Address keys alone cannot bound a distributed flood or separate tenants behind one address.§9resolvedsrc/domains/rate-limiting/rate-limiting.test.ts
18In-memory limiter tableBounded at 10 000 keys; a key leaves only when its window ends, never to make room. When full, overflow requests share one window.—resolvedsrc/domains/rate-limiting/rate-limiting.test.ts
19Concurrent exchangesCapped by GATEWAY_MAX_CONCURRENT_REQUESTS, because a body that never arrives holds a connection for the idle upload budget (five minutes). Overflow gets 503 gateway_overloaded.§11.2resolvedsrc/domains/proxying/exchange.test.ts
20Rate limits in productionProduction refuses to start with every limit off unless GATEWAY_ALLOW_NO_RATE_LIMITS=true: the limiter is the only flood protection.—resolvedsrc/runtime/env/env.test.ts
21Access revocationLatency is the decision-cache TTL (GATEWAY_ACCESS_CACHE_TTL_MS, 5 s). WebSocket revalidation skips the cache.—resolvedsrc/domains/workspace-access/workspace-access.test.ts
22Production stateSessions and limits live in Redis or Postgres unless GATEWAY_ALLOW_MEMORY_STATE=true declares a single node. In-memory state logs callers out at random and multiplies limits by replicas.§4.3resolvedsrc/runtime/env/env.test.ts
23Per-route cross-site policyRoute property crossSite. Cross-site navigations are allowed by default (deep links); strict suits upstreams that change state on GET. GATEWAY_CSRF_STRICT is the default.§8resolvedsrc/domains/configuration/configuration.test.ts, test/contract/policies.test.ts
24The caller's Authorization headerNot forwarded by default: the credential of the caller stays in the trust domain of the gateway, and a route that needs it sets forwardAuthorization: true.§7.1resolvedsrc/domains/forwarding/headers.test.ts, test/contract/policies.test.ts
25Revocation latencyRoute property accessCacheTtlMs; 0 makes revocation immediate. The cache key includes the route.§6resolvedsrc/domains/workspace-access/workspace-access.test.ts, test/contract/policies.test.ts
26WebSocket handshake header policyThe handshake follows the HTTP header policy: no gateway cookies upstream, gateway-written identity, caller headers not copied. It used to leak the session.§7.1, §10resolvedsrc/domains/forwarding/headers.test.ts, test/contract/policies.test.ts
27Upstream TLSUpstream certificates are verified with the deployment CA; the deployment certificate is presented on request. Certificate without key, or the reverse, stops startup. WebSocket upstreams use plain http.§11.1.2resolvedsrc/runtime/env/env.test.ts, test/network/tls.test.ts
28Identity signature formatA JWS with a detached canonical JSON payload, replacing the hand-rolled v3 digest so upstreams verify with one library call. Superseded by decisions 40 and 57 (ES256).§7.4supersededsrc/domains/forwarding/identity-signature.test.ts
29Cookie parsingA standards library parses and renders cookies, replacing two inconsistent parsers (RFC 6265). For a duplicated name the first value wins.—resolvedsrc/domains/forwarding/cookies.test.ts
30Limiter windowSliding, in at most 60 buckets whose boundaries every replica computes alike; a fixed window lets twice the limit through at a boundary.—resolvedsrc/domains/rate-limiting/window.test.ts, test/integration/redis-limiter.test.ts
31Shared-state clientThe redis package (cluster and Sentinel support), offline queue off so commands fail fast, connection in the background so the gateway starts while Redis is down.—resolvedsrc/platform/redis/client.test.ts, test/integration/redis-sessions.test.ts
32Shared-state backendRedis or Postgres by URL scheme, under one contract and one test suite (test/support/shared-state-conformance.ts), benchmarked side by side (scripts/ops/benchmark.ts).§4.1, §12.5, §12.6resolvedtest/integration/postgres-conformance.test.ts, test/integration/redis-conformance.test.ts, internals.md
33Postgres atomicityThe two Redis scripts are two Postgres functions; an advisory lock serializes one key, so the deployment counter is exact across processes.—resolvedsrc/platform/postgres/schema.ts, test/integration/postgres-replicas.test.ts
34Postgres durabilityGATEWAY_POSTGRES_SYNCHRONOUS_COMMIT=off by default: the state is a protection, not a ledger, and the limiter is on every request.—resolvedinternals.md, src/platform/postgres/client.ts
35Postgres row expiryExpiry is a column; adapters sweep, and reads ignore expired rows.—resolvedtest/integration/postgres-conformance.test.ts
36Runtime versionThe gateway refuses to start on Bun older than 1.4.3, checked before the environment, because on 1.2 it booted and then misbehaved.§4.2resolvedsrc/runtime/version.ts, src/runtime/version.test.ts
37Access outageAn explicit {"allow": false} is 403 workspace_forbidden (access_denied); no answer or an unreadable one is 503 access_unavailable with retry-after (gateway_access_unavailable_total), so outages are not mistaken for policy.—resolvedsrc/domains/workspace-access/decision.ts, src/app/dispatcher.ts, test/contract/endpoints.test.ts
38Bearer token typeBearer tokens must be access tokens: a token with a nonce (an id token of this client) is refused, unless disabled. A provider with a type claim names it in GATEWAY_OIDC_REQUIRED_TOKEN_CLAIMS.—resolvedsrc/domains/authentication/oidc.ts, test/integration/oidc.test.ts, test/contract/endpoints.test.ts
39Request budgetsThe three budgets are counted concurrently in one shared-state round trip, all counted even when one refuses, before authentication. The reported refusal is the first in spec order.—resolvedsrc/app/limits.ts, src/app/limits.test.ts, test/integration/redis-round-trips.test.ts
40Identity signature keyAsymmetric in production: P-256 private key in the gateway, public key in upstreams, ES256, version v5. Production refuses to start without the key.§7.4resolvedsrc/domains/forwarding/identity.ts, scripts/cli/identity-key.ts, test/contract/identity-signature.test.ts
41In-flight Access callsConcurrent requests for the same decision share one call, so cache expiry does not stampede the service.—resolvedsrc/domains/workspace-access/client.ts, src/domains/workspace-access/workspace-access.test.ts
42Path rewriteA route may declare its upstream path from placeholders and {rest}. Values keep the caller's encoding; the signature covers the rewritten path; routes without one are unchanged.—resolvedsrc/domains/routing/match.ts, src/domains/configuration/schema.ts, test/contract/rewrite.test.ts
43Request traceThe gateway writes its own W3C traceparent as a child span, logs the trace ID beside the request ID, and measures gateway_upstream_duration_seconds separately.—resolvedsrc/domains/forwarding/trace.ts, src/domains/proxying/exchange.ts, test/contract/trace.test.ts
44Route upstreamsA route may list upstreams, tried in order on connection failure for resendable requests only. The response budget covers the whole exchange; WebSocket handshakes use the first upstream.—resolvedsrc/domains/proxying/exchange.ts, src/domains/configuration/schema.ts, test/contract/upstreams.test.ts
45Access protocolPublished in spec §6.1, with a conformance suite and a production-ready reference implementation the suite runs against.§6.1resolveddocs/gateway/spec.md §6.1, test/support/access-conformance.ts, services/access-service.ts
46Running policyEach generation logs the configuration, each route, and a short list of warnings for permissive settings.§12.2resolvedsrc/domains/configuration/describe.ts, src/runtime/policy-log.ts, src/runtime/policy-log.test.ts
47Unresponsive route upstreamsWith a successor available, an attempt is abandoned after upstreamAttemptMs (2 s), resendable requests only, so a silent upstream no longer consumes the whole budget.§11.4resolvedsrc/domains/proxying/exchange.ts, test/contract/upstreams.test.ts
48Replayable bodyOn multi-upstream routes, a body with an announced length within replayMaxBytes (64 KiB) is held so the request can fail over; others stream once. Only idempotent methods are resent.§11.4resolvedsrc/domains/proxying/exchange.ts, src/domains/proxying/exchange.test.ts, test/contract/upstreams.test.ts
49Route methodsA route may name methods; among same host and path, a method match beats the fallback. Matching on headers or query is excluded so ambiguity stays checkable at load.§3.3resolvedsrc/domains/routing/match.ts, src/domains/routing/routing.test.ts, test/contract/methods.test.ts
50Access outage graceOpt-in accessStaleGraceMs (default 0) serves the last allow decision during an outage. Denials are never served stale, and fresh revalidations never use it.§6.2resolvedsrc/domains/workspace-access/client.ts, src/domains/workspace-access/workspace-access.test.ts, test/contract/access.test.ts
51Routes without a workspaceaccessCheck asks about the route (subject, route, roles) with no workspace; it requires an authentication mode other than none.§6.3resolvedsrc/app/dispatcher.ts, src/domains/workspace-access/client.ts, services/access-service.ts, test/support/access-conformance.ts, test/contract/access.test.ts
52Body of a request never resentA body is held only if it can actually be replayed (several upstreams, idempotent method, within the limit), so POST bodies stream and keep the early answer.§11.4resolvedsrc/domains/proxying/exchange.ts, src/domains/proxying/exchange.test.ts
53Release compatibilityBehaviour, access protocol, signature format, route-table format, variable and metric names are versioned promises (spec §14): MINOR extends, MAJOR changes with a migration. Code layout and log wording are not.§14.1, §14resolveddocs/gateway/spec.md §14, src/runtime/composition.ts, src/domains/forwarding/identity.ts
54Upstream healthA passive, per-process breaker moves recently failed upstreams to the end of the list, keeping file order within each group. It never probes, and state-changing requests are still sent once.—resolvedsrc/domains/proxying/breaker.ts, src/domains/proxying/breaker.test.ts, test/contract/upstream-breaker.test.ts
55Revoking a caller's sessionsSessions are indexed by subject (hashed in Redis), and a script, not an endpoint, revokes them. Both backends hold the index.—resolvedsrc/domains/authentication/adapters/redis-sessions.ts, src/domains/authentication/adapters/postgres-sessions.ts, scripts/cli/sessions.ts, test/support/shared-state-conformance.ts
56Cookies the gateway readsThe parser reads only what the renderer would forward: no nameless pairs, no control characters in names, control characters stripped from values. Found by property tests.—resolvedsrc/domains/forwarding/cookies.ts, src/domains/forwarding/cookies.test.ts, test/fuzz/parsers.test.ts
57Removing the symmetric identity formatv4, whose key every upstream shared, is removed in 1.0. Without GATEWAY_IDENTITY_SIGNING_KEY, non-production processes sign with a per-process key no upstream can verify.§7.4resolveddocs/gateway/spec.md §7.4, src/domains/forwarding/identity.ts, src/runtime/composition.ts, src/runtime/env/
58Freezing the 1.0 contractsThe spec §14.1 contracts (§3, §6.1, §7.4, §4 names, §12.1 names, §14.2 answers) are frozen; the review changed only the signature (decision 57).§14.1, §14.2, §14resolveddocs/gateway/spec.md §14, CHANGELOG.md
59Requests waiting on a dependencyGATEWAY_MAX_PENDING_REQUESTS (1 000) bounds requests inside the gateway, including those waiting on Access; overflow gets 503 gateway_overloaded at once. Tuned separately from the exchange cap.§2.4, §11.2resolvedsrc/app/admission.ts, test/contract/admission.test.ts
60Access healthA per-process breaker stops calling after GATEWAY_ACCESS_BREAKER_FAILURES (5) failures for GATEWAY_ACCESS_BREAKER_COOLDOWN_MS (5 s). It only refuses; denials and 400 are not failures; grace still applies.§6.4resolvedsrc/domains/workspace-access/breaker.ts, test/contract/access-breaker.test.ts
61Denial reason in metricsThe protocol carries an optional reason code, counted when present; the deprecated free-text rule remains for services without one.§6.5resolvedsrc/domains/workspace-access/decision.ts, test/contract/access-reason.test.ts
62WebSocket revalidation scheduleJittered: the first check is spread over the interval and periods vary within a 40 % band, so connections opened together do not hit the service together.§10resolvedsrc/domains/websockets/bridge.ts, src/domains/websockets/bridge.test.ts
63Unwritable caller identityA subject that cannot be written into a header gets an explicit 500 with the route logged, instead of a thrown error; the identity is never forwarded.—resolvedsrc/app/dispatcher.ts
64Bearer token lifetimeTokens must carry exp, and iat while the bound is on; lifetime (issue to expiry) is capped by GATEWAY_OIDC_MAX_TOKEN_AGE_MS (one day). Browser sessions are bounded by their maximum age.§5.4resolvedsrc/domains/authentication/oidc.ts, test/integration/oidc.test.ts
65Limits while the shared state is unreachableGATEWAY_RATE_LIMIT_FALLBACK: local (default, per-process windows), open (earlier behaviour) or closed. Fallback is counted in gateway_rate_limit_fallback_total and logged.§9resolvedsrc/domains/rate-limiting/fallback.ts, test/contract/limits-fallback.test.ts
66The caller in the logRequest and refusal lines carry the subject and workspace. The subject is an identifier, not a credential, so it is not redacted.§12.2resolvedsrc/app/dispatcher.ts, test/contract/logs.test.ts
67Postgres server versionBuilt and tested on PostgreSQL 18 but not enforced: the SQL works on 9.6 and later, and no spec boundary depends on the version.§12.5resolveddocs/gateway/spec.md §12.5, src/platform/postgres/schema.ts, .github/workflows/ci.yml, scripts/dev/postgres-dev.sh
68Deployment control planeAdministration is a separate process, gateway-control, on an internal socket with scoped tokens and an audit entry per write. The gateway has no admin endpoint, and only the control plane's shared-state role may write the route table.§12.7resolveddocs/gateway/spec.md §12.7, src/domains/control/policy.ts, src/app/control.ts, test/contract/control-plane.test.ts, test/integration/control-runtime.test.ts
69Route table in the shared stateGATEWAY_CONFIG_SOURCE=store: immutable, validated revisions plus an active pointer, compare-and-swap writes, polled at least once a second; the last valid table survives an unreachable store. Heartbeats list live replicas.§3.4resolveddocs/gateway/spec.md §3.4, src/domains/configuration/revisions.ts, test/support/shared-state-conformance.ts, test/integration/route-table.test.ts
70Route precedenceHost first, then method, so a catch-all naming GET can no longer shadow a host's own route.§3.3resolvedsrc/domains/routing/routing.test.ts, test/contract/precedence.test.ts
71Caller addressx-forwarded-for is read from the right, skipping GATEWAY_TRUSTED_PROXY_HOPS entries, so a caller cannot pick its own rate-limit key.§7.3resolvedsrc/domains/forwarding/context.test.ts, src/domains/forwarding/headers.test.ts
72Caller subjectWritten as is; an empty, control-character or over-512 subject is refused with 500, because sanitizing could merge two callers. E-mail and workspace are still sanitized.§7.1, §7.4resolvedsrc/domains/forwarding/identity-signature.test.ts
73Trusted proxy rangesEnvironment ranges replace the file's instead of adding to them; the sample trusts nothing.§3.1, §7.3resolvedsrc/domains/configuration/configuration.test.ts
74Redis shared-state usersThe ACL file is one rule per line, with ~, %W~ and %R~ patterns and no passwords; passwords are set with ACL SETUSER after start. An earlier file with comments and >~ patterns broke startup and published passwords.—resolvedtest/integration/least-privilege.test.ts, deploy/redis/gateway.acl
75Session cookie and bearer token in one requestThe token authenticates; cross-site rules still apply because the cookie was sent.§5.1, §8resolvedsrc/domains/authentication/policy.ts, test/contract/endpoints.test.ts
76Cross-site requests refused, navigations allowedState-changing cross-site requests are refused; safe navigations pass unless GATEWAY_CSRF_STRICT; cross-site /auth/logout is always refused.§8resolvedsrc/domains/forwarding/origins.ts, test/contract/policies.test.ts
77Pipeline orderGateway endpoints first; in the dispatcher, route matching precedes the limiters, which count unmatched requests before refusing them.§2.3resolvedsrc/app/dispatcher.ts, test/contract/pipeline.test.ts
78Upstream URL with a base pathThe base path prefixes the request path, the request query replaces the upstream's, fragments drop; an upstream URL with a query or fragment fails at load.§3.1, §11.1resolvedsrc/domains/forwarding/target.ts, test/contract/rewrite.test.ts
79Failure while the body streamsThe body ends early and the outcome is aborted; no JSON error replaces a started response.§11.3resolvedsrc/domains/proxying/exchange.ts, test/contract/endpoints-failures.test.ts
80Meaning of bodyMsIdle budget, reset per chunk, governing the upload; responseMs and firstByteMs are totals.§11.2resolvedsrc/domains/configuration/schema.ts, test/contract/limits.test.ts
81Budget namesconnectMs became responseMs and headersMs became firstByteMs; the old names are refused.§11.2resolvedsrc/domains/configuration/schema.ts, src/domains/configuration/configuration.test.ts
82Upstream answers before the upload endsThe upstream's answer wins; 413 only for a failure found before it.§11.3resolvedsrc/domains/proxying/exchange.ts, test/contract/endpoints.test.ts
83WebSocket handshake originWith the session cookie, an allowed origin is required (missing is refused), regardless of GATEWAY_CSRF_STRICT, before the upstream is contacted.§8, §10resolvedsrc/domains/forwarding/origins.ts, test/contract/websocket-revocation.test.ts
84Browser cookieRefreshed on every session request with Max-Age = the smaller of remaining idle lifetime and remaining maximum age.§5.3resolvedsrc/domains/authentication/cookies.ts, test/contract/forwarding.test.ts
85Request identifierA valid incoming x-request-id is reused, otherwise a UUID is generated, and it is returned.§2.1resolvedsrc/app/app.ts, test/contract/endpoints.test.ts
86Maximum age of 0No cap.§5.3resolvedsrc/domains/authentication/session.ts, test/contract/endpoints.test.ts
87Response without a bodyfirstByteMs is not armed for HEAD or bodiless statuses.§11.2resolvedsrc/domains/proxying/exchange.ts, test/contract/endpoints.test.ts
88103 and 101103 is skipped for the final response; 101 on the HTTP path is refused.§11.2resolvedsrc/domains/proxying/exchange.ts, test/contract/endpoints.test.ts
89OIDC token validationThe browser flow requires nonce, single-use state, PKCE verifier and id token, with fixed cookie attributes.§5.2, §5.4, §5.5resolvedsrc/domains/authentication/oidc.ts, test/integration/oidc.test.ts
90Maximum session lifetimeIdle lifetime plus a maximum age from creation; the maximum age is the real bound.§5.3resolvedsrc/domains/authentication/session.ts, test/contract/endpoints.test.ts
91Readiness after a failed reloadStays ready on the last valid configuration; the failure shows in /readyz, logs and metrics.§12.3resolvedsrc/domains/configuration/revisions.ts, test/contract/lifecycle.test.ts
92Code organizationDomains hold policy, use cases, ports and adapters; app, platform and runtime are explicit boundaries checked in CI (internals.md).—resolvedsrc/app/app.ts, test/architecture/imports.test.ts