Skip to main content
Spillway is one Node process with two HTTP planes in front of one Postgres database. There’s no separate policy service, no message queue in the request path, no sidecar — governance checks run in-process, synchronously, before the provider is ever called.

The two planes

Data plane (/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.
Every response carries an 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 by JOBS_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 an X-Spillway-Org header — 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.