The two planes
/v1/*) is the gateway. A client points its OpenAI or Anthropic SDK here;
Spillway authenticates the virtual key, evaluates guardrails and budgets, dispatches to the
real provider, streams or returns the response, and records what it cost.
Control plane (/api/*) is the governance and administration API — the dashboard’s
backend and the machine-readable equivalent for orgs, teams, keys, budgets, policies,
routing rules, approvals, automation rules, and reports.
The two planes share only the shared request/response schemas and the database — the
control plane never touches the data plane’s request path and vice versa. CORS is
registered per-plane, not globally, so a misconfigured origin on one plane can’t leak into
the other.
Request lifecycle (data plane)
A/v1/chat/completions or /v1/messages request moves through a fixed pipeline, each
stage a plain function over a shared PipelineContext:
1
Auth
Parse the virtual key from
Authorization: Bearer or x-api-key, hash it, and load its
policy bundle (cached briefly). Not-found, revoked, and expired all return the same
401 key_not_found so a key can’t be probed for existence; a database error during the
lookup returns 503, never a false 401 — the gateway fails closed, not open.2
Validate
Parse and validate the request body, apply the key’s allow-lists, and clamp
out-of-range parameters.
3
Guardrails
Evaluate matching guardrail policies (
/api/policies). A policy can deny the request
outright or flag it for visibility. (A third effect, require_approval, is defined but
not yet available — see Guardrails.)4
Budget check
Check every budget scope the request falls under (virtual key, team, org, provider). A
budget in
enforce mode that would be exceeded blocks the request here — before any
provider call.5
Route
Resolve the target provider and model, applying routing rules (rewrite/fallback),
aliases, and residency constraints.
6
Dispatch
Decrypt the provider key, transform the request for the target provider, call upstream,
and fork on status: a 2xx is sent to the client immediately; a non-2xx is normalized
into a Spillway-shaped error.
7
Reconcile
Compute cost from the provider’s usage payload and write the request row — after
the response has already been sent to the client, so the caller never waits on the
audit write. This is also where spend counters (what budgets check against) are
incremented, inside a transaction scoped to the request’s org.
x-spillway-request-id header that matches the requests.id row
written by Reconcile — the starting point for tracing any request after the fact.
Providers
The provider registry currently has adapters for OpenAI, Anthropic, Gemini, and a generic OpenAI-compatible adapter for custom upstreams. A request arriving in one dialect (say, the Anthropic/v1/messages shape) can be routed to a model on a different
provider — Spillway cross-translates the request and response shapes so the client’s SDK
never has to know.
Streaming (SSE) is passed through untouched, teed so usage can be captured from a stream
without buffering the whole response, with a token estimator as a fallback for providers
that don’t report usage on every chunk.
Governance building blocks
These are separate resources with separate CRUD endpoints under/api/* — see
Configuration for the field-level reference on each:
Data layer
- Postgres, one database, row-level security (RLS) on every table — multi-tenancy
is enforced at the database, not just in application code. A new migration that doesn’t
add RLS and a policy fails CI (
pnpm migration-lint). - Money is decimal, never float. Costs are computed as bigint micro-USD and formatted to a fixed-precision numeric string at the database edge.
- Provider keys are encrypted at rest (AES-256-GCM) and only decrypted in-process, immediately before a provider call.
- Two database roles: an app role (subject to RLS, used by the running server) and a jobs role (broader read/delete grants, no RLS bypass) for background jobs — deliberately separate so a compromised app credential can’t touch what the jobs role can.
Background jobs
A small set of scheduled jobs run alongside the server: alert delivery, anomaly-detection scans, chargeback/insights aggregation, and data retention. They run in-process on a scheduler, gated byJOBS_ENABLED, and use the jobs database role rather than the app
role.
Auth
- Humans (dashboard) authenticate via WorkOS AuthKit — a session JWT verified against WorkOS’s JWKS.
- Machines authenticate the data plane with a virtual key (
mk-live-…) and the control plane with either a session token or an admin key (mk-admin-…) plus anX-Spillway-Orgheader — though admin-key auth isn’t wired up yet; see the API reference for current status.
Where next
Configuration
Create budgets, guardrails, routing rules, and approval chains.
Troubleshooting
Common failure modes and how to trace a request.