Console
Sign in
Docs for Console and CLI user
On this page
  1. Code layout
  2. Domains
  3. Import rules
  4. Resource ownership
  5. Request flow
  6. Source map
  7. Shared-state layout
  8. Verification
  9. Capacity
  10. Shared-state backends
  11. Runtime
  12. Release
  13. Known gaps

Internals

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

For gateway maintainers: code layout and import rules, resource ownership, the source map, how the gateway is verified, capacity measurements, the runtime capabilities it relies on, release and known gaps. None of this is external contract; behavior is normative in spec.md.

Read bySecurity engineerRepository maintainer

Code layout#

The gateway is a modular monolith: one deployable, split into domains with explicit boundaries. bun run architecture checks the boundaries in the test suite and CI.

PathOwns
src/main.tsGateway process entry point. Starts the runtime and nothing else.
src/control-main.tsControl-plane process entry point. Starts the control-plane runtime and nothing else.
src/app/The Hono application: thin handlers, middleware, pipeline order, mapping failures to protocol responses.
src/domains/<domain>/A domain's policy, use cases, ports and unit tests.
src/domains/<domain>/adapters/Port implementations; only the composition root uses them.
src/platform/bun/Listener, actual peer address, outbound HTTP transport, upgrade and socket primitives.
src/platform/redis/Construction, TLS settings and disposal of the shared Redis clients.
src/platform/postgres/Construction, pool, TLS settings, schema and disposal of the shared Postgres clients.
src/runtime/Environment, composition root, file watcher, signals, drain, running-policy log.
src/observability/Logging, metrics and observation handles. Knows nothing else.
services/Access reference implementation: a runnable deployment component, not a fixture.
services/fixtures/Deterministic identity provider, Access and upstream stand-ins; refuse to run in production; not in the image.
scripts/cli/ (identity key, migrations, session revocation), dev/ (runner, test Redis and Postgres), gates/ (architecture, version, environment), ops/ (load, benchmark), support/.
test/Checks that are not a domain's unit tests. Shared helpers are in test/support/.

Domains#

DomainOwnsMay use
src/domains/routing/Route identity, host and path matching, parameters, precedence, ambiguity detection.—
src/domains/configuration/File schema, strict validation, effective defaults, immutable compiled snapshots, atomic last-good publication.routing, authentication
src/domains/authentication/Principal, mode policy, credential selection, OIDC and JWKS, browser login and logout, signed state, sessions, expiry, cookies.—
src/domains/workspace-access/Workspace validation, Access requests and decisions, cache keys, TTL, cache adapters.authentication, routing
src/domains/forwarding/Peer and proxy trust, cross-site policy, header and cookie filtering, target URL, sanitized identity, signature.authentication
src/domains/rate-limiting/Sliding windows, scopes, retry-after, bounded memory, shared-state counters.—
src/domains/proxying/Exchange state, body limits, phase and idle budgets, cancellation, commitment outcomes, active-exchange registry.forwarding
src/domains/websockets/Slot reservations, upstream-first handshake, bridge state, buffers, keepalive, revalidation, pair registry.forwarding
src/domains/control/Control-plane token scopes, routes a write may not change, administrative write rules, failed-authentication counter.configuration, authentication, workspace-access

Import rules#

scripts/gates/architecture.ts enforces these in the test suite and CI:

  1. Import a domain only through its index.ts.
  2. Use only the dependencies the domain matrix allows.
  3. A domain's core (outside adapters/) imports no framework, Bun API, application, platform, runtime or adapter; dependencies arrive through ports.
  4. A domain's index.ts exports no adapter.
  5. The application uses domain entry points and observation interfaces, never an adapter.
  6. The platform implements ports, makes no policy decision, and imports neither application nor runtime.
  7. The observation layer imports nothing from the project.
  8. The runtime is the composition root and may assemble everything.
  9. No catch-all directories (utils, services, repositories, common, …) and no import cycles, type-only imports included.
  10. Only the application uses Hono; only the platform uses runtime APIs.
  11. The source imports nothing from services/: neither the Access reference implementation nor the test fixtures belong to the gateway, and a source file that imported one would put the test accounts in the image.

Environment variable names never leave src/runtime/env/; the rest of the gateway receives the settings it needs, not the environment. scripts/gates/environment.ts checks that .env.example and Configuration match the variables declared there.

bun run dead runs knip --use-tsconfig-files and fails on any unused file, export, type or dependency. A domain's index.ts is not an entry point, so an export no consumer imports is reported, as is an export used only inside its own module. Workers started with Bun.spawn and the bun --watch runner are declared as entries in the knip section of package.json.

The gateway adds one rule to the platform working rules: a request is an exchange, not a chain of middlewares.

Resource ownership#

LifetimeOwner
Processsrc/runtime/composition.ts (construction), src/runtime/lifecycle.ts (disposal)
Configuration generationConfiguration domain: immutable snapshot, published atomically
RequestApplication: captures one generation
HTTP exchangeProxying domain, with the Bun transport
WebSocket pairWebSockets domain, with the Bun socket primitives

The runtime constructs and closes the shared clients, watchers and process timers. The configuration owns the last-good immutable generation; each request captures one generation for all its policy and forwarding decisions. An HTTP exchange owns its streams, phase timers and cancellation. A WebSocket pair owns both legs, its buffers, its keepalive timers and its slot.

A single owner finalizes each exchange and each connection exactly once. A middleware completing is not an exchange completing.

Request flow#

text
Bun.serve (platform/bun/listener.ts)
  └─ app.fetch (app/app.ts)
       └─ middleware: request id, cookies
            ├─ gateway endpoints (app/endpoints.ts)
            └─ dispatcher (app/dispatcher.ts)
                 ├─ routing: matchRoute
                 ├─ rate-limiting: the four scopes
                 ├─ authentication: selectCredential, authenticate
                 ├─ forwarding: checkCrossSite, buildUpstreamHeaders
                 ├─ workspace-access: decide
                 ├─ proxying: execute  →  platform/bun/http-transport.ts
                 └─ websockets: handle   →  platform/bun/upstream-websocket.ts

The dispatcher is the only place domains are combined; each decides only for itself.

Source map#

Each rule lives in one domain, with its tests beside it.

PathOwns
app/app.tsHono app, request-id middleware, gateway endpoints.
app/dispatcher.tsPipeline order (spec §2.3).
app/admission.tsBound on requests inside the gateway.
app/limits.tsThe three §9 budgets, counted together in one shared-state round trip.
app/endpoints.ts/healthz, /readyz, /metrics, /auth/login, /auth/callback, /auth/logout.
app/revalidation.tsRechecking route, session and access on an open WebSocket.
app/dependencies.ts, state.ts, errors.ts, cookies.tsApplication interfaces, per-request state, refusal body, cookie reading.
routing/schema.ts, match.tsRoute shape; compilation, precedence, matching, ambiguity.
configuration/schema.tsFile schema, strict validation, defaults.
configuration/store.tsGenerations: snapshot, atomic publication, last good state, reload status.
configuration/describe.ts, compile.ts, routes.tsRunning-policy description; compiling and diffing tables; effective routes.
configuration/revisions.ts, adapters/Revision shape and store contract; Redis and Postgres route tables (a revision and its trail entry in one script or transaction).
authentication/policy.ts, principal.tsModes; credential selection (invalid is rejected, never replaced).
authentication/oidc.ts, login-state.tsRelying party (discovery, PKCE, exchange, bearer validation); signed login state.
authentication/session.ts, cookies.ts, service.tsSession rules; cookie names; authenticate, login, logout.
authentication/adapters/Memory, Redis (one script) and Postgres (one function) session stores.
workspace-access/client.tsAccess client: request, cache, fail closed.
workspace-access/breaker.ts, decision.ts, workspace.tsService health; decision shape and metric reason; workspace ID validation.
workspace-access/adapters/Memory, Redis and Postgres decision caches.
forwarding/network.ts, context.tsCIDR and trusted proxies; peer, client address, protocol and effective host.
forwarding/origins.ts, headers.ts, cookies.tsCross-site policy; header policy both ways; cookie parsing and rendering.
forwarding/identity.ts, trace.ts, target.tsIdentity signature; trace context; upstream URL and Location rewrite.
rate-limiting/policy.ts, window.ts, limiter.ts, fallback.tsScopes and keys; sliding window; bounded in-memory limiter; outage fallback.
rate-limiting/adapters/Redis and Postgres limiters (one script or function; fail open).
proxying/transport.ts, exchange.ts, breaker.tsTransport ports; exchange state machine; passive upstream breaker.
websockets/Ports, upgrade context, connection slots (reservation.ts), relay (bridge.ts), handshake order (service.ts).
control/policy.ts, service.ts, adapters/Token scopes and protected routes; write rules and trail; failed-authentication counters.
platform/bun/listener.ts, http-transport.ts (no redirects, buffering or decompression), access-transport.ts, client-upgrade.ts, upstream-websocket.ts, runtime.ts (reads the runtime version).
platform/redis/, platform/postgres/client.ts (server shapes, pool, TLS, background connection), commands.ts, postgres/schema.ts.
runtime/env/settings.ts, schema.ts (variable names), read.ts; the only reader of process.env.
runtime/shared-state.ts (backend by URL scheme), composition.ts, lifecycle.ts, policy-log.ts, version.ts.
observability/logging.ts (credential redaction), metrics.ts (bounded labels).

services/access-service.ts answers the spec §6.1 protocol from a policy file. It is not a fixture; test/support/access-conformance.ts runs against it, so protocol and reference cannot drift.

ChangeLands in
Authentication modeauthentication/policy.ts, then principal.ts
Identity headerforwarding/identity.ts, then headers.ts
Limit scoperate-limiting/policy.ts, then app/dispatcher.ts
Metricobservability/metrics.ts
Environment variableruntime/env/schema.ts, .env.example, Configuration
Exchange phaseproxying/exchange.ts, spec §11.2
Route-table fieldconfiguration/schema.ts, and routing/schema.ts if it affects matching

Shared-state layout#

Postgres objects, in the gateway schema:

ObjectHolds
gateway.sessionsSessions: jsonb record, expiry and subject columns.
gateway.access_decisionsCached decisions.
gateway.rate_limit_bucketsLimiter buckets.
gateway.session_touch(...)Read a session and refresh its expiry in one transaction (Redis: TOUCH_SCRIPT).
gateway.rate_limit_check(...)Count a request in one transaction (Redis: WINDOW_SCRIPT).
gateway.config_revisions, gateway.config_active, gateway.auditRoute table, active pointer, write trail.

The Redis ACL in deploy/redis/gateway.acl gives gateway read/write on session, limit and decision keys, write-only on its heartbeat (%W~gateway:cfg:hb:*), and read-only on gateway:cfg:active, gateway:cfg:rev:* and gateway:cfg:revs. Revisions are hashes and history a list, and gateway can neither write a hash nor push to a list. gateway-control may read and write ~gateway:*.

Verification#

KindCommandProves
Unit and contractbun test src test/contractDomain policy; the pipeline over a socket with real fixtures.
Networkbun test test/networkThe runtime capabilities proxying needs.
Integrationbun test test/integrationShared state on real Redis and Postgres, the shared backend contract, several replicas.
Browserbun test test/browserCookies and origins in Chromium.
Interopbun test test/interopDiscovery and sign-in against dex.
Fuzzbun test test/fuzzParser properties.
Soakbun test test/loadResources settle after traffic.
Architecturebun test test/architectureImport boundaries, environment docs, tool coverage.
Loadbun run loadCapacity.
Backend comparisonbun run benchmarkNot in CI.
Types, lint, format, dead codebun run typecheck, lint, format:check, dead—

CI (.github/workflows/ci.yml) also builds the image, checks it starts with a deployment configuration, refuses to start without a session secret, Access, identity provider or rate limit, and contains no fixtures.

Last full run: 793 passed, 0 failed across 79 files on Bun 1.4.3, with real Redis 7.0.15 (GATEWAY_TEST_REDIS_URL=redis://127.0.0.1:6399/0), real Postgres 18.6 (GATEWAY_TEST_POSTGRES_URL=postgres://postgres@127.0.0.1:5432/gateway), 3 Chromium tests and 2 soak tests. Only the four real-provider interop tests were skipped (CI runs them against dex). Coverage of src/: 93.8 % lines, 93.0 % functions (mean over files); whole tree: 91.7 % lines, 91.8 % functions. Without Redis, its 38 tests are skipped; without a named Postgres server, Postgres tests use the in-process engine.

Capacity#

Measured with scripts/ops/load.ts, which also reports resident memory (size pods with it) and can target a running gateway:

bash
bun run load -- --requests 20000 --concurrency 100
bun run load -- --websockets 1000
bun run load -- --url http://localhost:8080 --host api.example.com
TargetValue
Route requests1000 req/s per replica, p99 below 100 ms
Rate limitsDeployment-wide, not per replica (shared state)
WebSockets1000 open per replica, 500 opened/s
MemoryBelow 512 MiB per replica with 1000 WebSockets (pod limit 1 GiB)
ErrorsBelow 0.1 % of requests
DrainConnections close within GATEWAY_SHUTDOWN_TIMEOUT_MS

Reference machine: AMD Ryzen 7 5800X (8 cores, 16 threads), 31 GiB, Linux 7.2, Bun 1.4.3. Gateway and upstream in one process, plain http over loopback, idle machine. Numbers are a lower bound on cost: no ingress, TLS or network.

RunResult
3000 requests, 50 in flight7541 req/s, p50 6.4 ms, p90 7.7 ms, p99 8.8 ms, 0 errors
20000 requests, 100 in flight7683 req/s, p50 13.5 ms, p90 16.7 ms, p99 19.3 ms, max 21.6 ms, 0 errors; 150 MiB peak
500 WebSockets415 connections/s, handshake p99 193 ms, 0 errors
Idle71 MiB resident
1000 open WebSockets110 MiB; 642 connections/s, handshake p99 540 ms, 0 errors
  • About 7600 req/s; latency grows with requests in flight because the runtime queue is the bottleneck.
  • A WebSocket handshake costs about 2 ms; 1000 connections cost about 40 MiB.
  • On the same machine under other load: about 4400 req/s (p50 23 ms) at 100 in flight. Measure on your own machines.
  • Soak (bun test test/load, 5 rounds of 50 requests, a WebSocket and a reload): settled in 1.1 s, heap growth below 64 MiB. It is a release gate: memory, descriptors, connections and timers must settle.

Shared-state backends#

scripts/ops/benchmark.ts runs each scenario against both backends:

bash
bun run benchmark -- --redis redis://127.0.0.1:6399/0 --postgres postgres://127.0.0.1:5432/gateway

Scenarios: no-state (baseline), limits (three limiter checks), session (plus the session read), full (plus the cached decision). Last run: 20 000 requests at 50 in flight, two rounds, all servers on the reference machine over loopback; an earlier run differed by a few percent.

ScenarioBackendreq/sp50p99
no-statein process72446.9 ms11.7 ms
limitsin process68347.2 ms11.7 ms
limitsRedis58848.4 ms12.9 ms
limitsPostgres381113.0 ms18.4 ms
sessionin process62267.8 ms13.5 ms
sessionRedis414411.7 ms26.4 ms
sessionPostgres274718.0 ms25.3 ms
fullin process56108.4 ms14.2 ms
fullRedis368013.4 ms20.1 ms
fullPostgres241420.6 ms27.9 ms
  • Redis costs about a tenth of baseline on limits and a third on full; Postgres about half, with twice Redis's p50. The gap grows with round trips.
  • Not the disk: with synchronous commit off, a limiter transaction takes about 0.2 ms instead of 1.8 ms. With it on, Postgres limits drops to about 420 req/s (p50 120 ms), because the deployment scope serializes every request.
  • Forty pool connections measured slower than ten (about 3 600 vs 4 100 req/s on limits): serialization, not pool size, is the bottleneck.
  • Not measured: an ingress, TLS (the ingress's cost), state on another host, managed servers, several replicas (counters are proved exact; capacity is the sum of replicas).

Runtime#

The gateway is written against Bun directly, so each boundary behind spec §4.2, §10 and §11 is proved by a test/network/ test against a real socket. Before reading its environment, src/runtime/version.ts stops the gateway on a runtime older than Bun 1.4.3 (decision 36); src/runtime/version.test.ts tests it.

CapabilityProofBehaviour
Streaming uploadtest/network/transport.test.ts: upstream polls chunks while the client producesBytes reach the upstream during the upload.
End of uploadSame test, /uploadEnd of the request stream is the reported event; peer acceptance is not claimed.
Response phasesRaw server sends the head, then the body a second laterTransport resolves on final headers (1 ms); first body byte at 1002 ms, so responseMs and firstByteMs are separate.
Interim response103 Early Hints, then finalOnly the final response is reported and forwarded.
Protocol switch101 on plain HTTPRefused: transport fails, the exchange answers 502.
Early response/early answers without reading the bodyThe answer arrives mid-upload; the body producer is cancelled.
Caller cancellationExchange signal abortedUpstream request cancelled, transport rejects.
Redirect302Not followed.
Compressed responsecontent-encoding: gzipBun decompresses by default, so the transport sets decompress: false; bytes pass unchanged.
Upstream WebSocket refusal403 instead of a handshakeReported with status, headers and body, not as a network failure.
Upstream subprotocolFirst offered is selectedReported and mirrored to the caller.
WebSocket over TLSBun's client against Bun and OpenSSL TLS serversFails with no cause, so the gateway refuses with reason tls. WebSocket upstreams use plain http.
Caller upgradeRaw client reads the 101Carries gateway headers (request ID, refreshed cookie).
Upstream TLStest/network/tls.test.ts, client certificate requestedGateway presents its certificate and verifies the upstream's against the deployment CA; a rejected certificate gives 502.
VersionResult
1.4.3Every boundary observable. The release version.
1.2.23Early response arrives but the body producer is not cancelled; the ws client's unexpected-response event is missing, so upstream refusals are invisible; the caller upgrade with gateway headers is refused.
1.2.0Early response never arrives: the exchange waits for the upload to end.

Release#

The release process is platform-wide: see platform release. Gateway binaries embed the runtime and are built for seven targets: bun-linux-x64 (glibc), bun-linux-x64-musl (Alpine), bun-linux-arm64, bun-darwin-x64, bun-darwin-arm64, bun-windows-x64, bun-windows-arm64. Archives are named like gateway-1.0.0-linux-x64-musl. The image is built twice, once into the local store for deployment checks and once for the registry; only the second carries provenance and SBOM attestations, because the local docker exporter cannot carry an attestation manifest list.

Known gaps#

Outside the gateway's scope, closed by the deployment:

GapClosed by
Client TLSThe ingress.
Upstream fixed per routeA router in front of pods, when the workspace picks the pod.
Revocation waits for the cache TTLGATEWAY_ACCESS_CACHE_TTL_MS, or accessCacheTtlMs: 0 per route.
Uncached decision latencyA fast Access; the cache absorbs the rest.
One hostname per product surfaceSubdomains, or one host with distinct path prefixes.
WebSocket upstream over TLSNot possible on this runtime (above).

Thin evidence:

AreaStatus
HistoryYoung project, a few days' design, never in production; every capacity number is from one machine.
In-flight access callsShared per replica: a burst across N replicas asks N times, bounded by the cache lifetime.
Access and upstream breakersPer process: a replica that saw no failure keeps calling and pays the budget.
Behind an ingress, remote shared stateNot measured.
Other providersTested with the project's provider and dex only; other claim names or id tokens without a nonce are untested.