Architecture
The Gateway
The internet-facing tenant tier — the trust boundary, its middleware stack, the cluster client, and plan-driven serving
The Gateway is the component above Nova that the internet is allowed to reach. It terminates TLS, verifies tenant tokens at the edge, serves the HTTP and SSE surface, and dispatches into the fleet over gRPC, while novad itself is never internet-exposed. It ships as its own binary, novagateway, with its own release cadence.
The trust boundary
What the Gateway holds, and refuses to hold, defines the boundary:
| It holds | It never holds |
|---|---|
| The caller's own forwarded bearer token, per request | Cluster membership or any coordination-plane access |
| Optionally, a read-only storage identity, used only to fetch settled bytes for raw read plans | A peer credential, or any credential that could write |
| Caches: remembered stream holders, decoded footers | Durable state of any kind; nothing survives a restart |
Verify, enforce, dispatch: the process is stateless and disposable. That is also why fleet discovery is deliberately dumb: the --addr seed may be one collective DNS name, and the Gateway learns nothing about the fleet except what redirects teach it. The design rejected reading membership from the metastore outright: it would hand the internet tier the coordination plane.
Anatomy
The HTTP surface is implemented exactly once, in a shared handler package over the generated data-plane client interface. The data bridge on novad binds those handlers to the node's own plane; the Gateway binds them to its cluster client and stacks tenant middleware in front. The two hosts cannot drift apart, because they are the same handlers.
Each layer has one job:
- TLS. Optional twice over: a certificate pair arms termination with a 1.2 floor; empty serves plaintext behind a terminating load balancer.
GET /healthzanswers outside the whole stack, so a target check never depends on the fleet it is deciding to route around. - CORS. Gateway business only: named origins, default deny, preflights answered at the edge. Tokens travel in the
Authorizationheader only, never query parameters. - Verification. The same scoped token, verified twice: cheaply at the edge (a 403 before any bytes move) and authoritatively by the star that serves the call. Forwarding never requires edge verification (the star stays authoritative), but acting on claims always does: startup fails closed if a claim-consuming feature (limits, host checks) is armed while verification is off.
- Host grammar. Under a configured DNS zone,
{bucket}.{zone}derives the bucket from the Host's first label; the reservedapi.{zone}serves the path-style form unchanged. Hosts outside the zone pass through untouched: a load-balancer health check is not a tenant surface. - Limits. Ceilings ride the token as claims (
nova_aux.limits:rps,connections,read_Bps,write_Bps), enforced per token subject, per instance, in a bounded limiter population with eviction. An over-budget request sheds429with a log line; an SSE stream at an exhausted byte budget pauses: backpressure, never a silent kill. The global effective limit is ≈ claim × replicas, honestly documented; there is no shared quota store, because exact global limits would put a stateful dependency in the hot path.
Library versus binary
Verification, the host grammar, CORS, and limits are Gateway middleware in the library; the novagateway binary does not bind flags for them yet: it forwards without verifying, serves path-style on any host, and serves no browser origins. The star's own verification and scope enforcement hold regardless. See the novagateway reference.
The cluster client
Dispatch rides a thin, dedicated client over the fleet: a connection pool over the configured addresses, NOT_OWNER redirect-following on unary verbs and at subscribe-open, and a bounded holder cache. The first touch of a stream pays one bounced request; steady state is client → Gateway → holder, with no hop left to remove. The Gateway forwards the inbound bearer verbatim on every call and adds no identity of its own.
The cluster client holds no cursor state. A mid-stream break surfaces to the SSE layer, which closes with a retryable event; the Last-Event-ID reconnect resumes exactly, because the shared handlers already emit a resume cursor per record. The same design absorbs credential rotation: a subscription ends at token expiry with an auth-expired event, and the refreshed client resumes where it left off.
Redirect targets are the stars' advertised addresses, dialed verbatim: each star's --advertise must resolve and route from the Gateway's network position, the same constraint every SDK client imposes.
Plan-driven serving
For settled history the fleet adds nothing to the bytes' transit, so the Gateway serves it without the fleet. A decorator in front of the shared handlers plans first for every read and SSE catch-up: it asks a star for a read plan, streams the vended settled entries itself, then opens the ordinary Read or Subscribe at exactly the residual's start, for one seamless response, with the handlers none the wiser.
The pipeline is bounded end to end: --fetch-inflight (default 64 MiB) is one byte pool over every concurrent response (a slow consumer or store waits its turn, never balloons memory), and --footer-cache-bytes (default 32 MiB) budgets decoded footers, each entry living at most the plan's lifetime. Presigned fetches route through the object cache when one is configured, falling back to the direct URL on any obstacle. Raw plans additionally need --storage-bucket (the read-only storage identity); without it they are proxied. --serve-plan=false is the kill switch that proxies everything through the fleet.
The feature's success metric is the offload ratio (nova.gateway.vended.bytes against .proxied.bytes), with fetch-wait age as its health signal.
Deploying it
The Gateway is a bandwidth tier: every client byte crosses its network interface twice, in from the client and out to the fleet, or the reverse for reads it vends. Scale it horizontally, and on Kubernetes give it a dedicated pool per location rather than co-locating it with stars; the controller's Gateway resource renders per-zone deployments behind one service, with an optional ALB door for internet exposure. See Kubernetes for the resource and the door, and the compose stack for the local instance at localhost:8080.
HTTP API guide
Using the surface the Gateway serves: JSON, SSE, hosts, limits
novagateway reference
Every flag, environment variable, and default
The object cache
The read-through tier the Gateway's vended fetches ride
Authentication
Scoped tokens: the grammar the edge and the star both enforce
Source documents
docs/design/022-gateway.mddocs/design/021-http-surface.mdinternal/gateway/cmd/novagateway/main.go