---
title: "Gateway overview"
description: "The Rust data plane: how a request is authenticated, authorized, dispatched, and metered on the way to a model or a tool."
---
The gateway (`apps/gateway`, crate `obol-gateway`) is the only component your
agents talk to. It is an axum service that terminates agent traffic on `/v1/*`
and `/mcp`, decides whether the call is allowed, injects your vendor credential
on the outbound hop, and records what happened.
Everything except `main.rs` lives in the library, so integration tests drive the
same router production serves.
## Surfaces
| Route | Method | Purpose |
|---|---|---|
| `/v1/chat/completions` | `POST` | OpenAI chat completions |
| `/v1/responses` | `POST` | OpenAI responses |
| `/v1/embeddings` | `POST` | OpenAI embeddings |
| `/v1/messages` | `POST` | Anthropic messages |
| `/v1/route` | `POST` | Advisory capability resolver — never dispatches |
| `/mcp` | `POST`, `GET`, `DELETE` | Streamable HTTP MCP server |
| `/webhooks/{workspace}/{connection}/{subscription}` | `POST` | Vendor webhook ingest (also mounted at `/hooks/...`) |
| `/healthz` | `GET` | Liveness — always `200` |
| `/readyz` | `GET` | Redis reachable, at least one policy snapshot loaded, not draining |
| `/startupz` | `GET` | Redis reachable and not draining; empty snapshots OK |
| `/metrics` | `GET` | Prometheus text exposition |
| `/internal/...` | `POST` | Control-plane-only endpoints, authenticated by service JWT |
The `/v1/*` LLM surface is documented in [LLM routes](/gateway/llm-routes); the
MCP surface in [MCP surface](/gateway/mcp).
## The five hot-path rules
The gateway is deliberately narrow. Five constraints shape every route.
A virtual key is hashed with SHA-256 and looked up at `key:{hash}` in Redis
(60-second TTL by default, `OBOL_KEY_TTL_S`). On a miss the gateway calls the
control plane's internal snapshot API with a short-lived service JWT and writes
the result back. The gateway holds no Postgres pool on the request path
(ADR-0006). A store failure fails closed as `not_ready` — never as
`unauthorized`, which a client would treat as final.
CEL decides visibility, Cedar decides permission, and both run inside the
gateway process against a snapshot already in memory. There is no network hop
and no database query per decision (ADR-0003). See
[CEL prefilter](/policy/cel-prefilter) and [Cedar](/policy/cedar).
The encrypted credential travels in the workspace snapshot. `obol-vault`
decrypts it (AES-256-GCM under a KEK) immediately before the outbound request
and the plaintext exists only between that call and the socket. It is never
serialized, cached, or logged. See [Vault](/security/vault).
OAuth exchange orchestration, billing, approvals inboxes, and connector adapters
live elsewhere. Python connectors are out-of-process MCP workers (ADR-0002,
ADR-0007).
Usage events go through a bounded in-memory queue and a batching flusher onto a
Redis stream. If that queue is full, the event is dropped and a counter
increments — a response is never blocked on metering. Invocation receipts and
verification events do not use that queue: they are committed durably through
the idempotency store before the gateway exposes a result (ADR-0032).
## Request lifecycle
Both surfaces share the same prelude: request id, authentication, workspace
snapshot, environment and freeze checks, scope check. They diverge after that.
```mermaid
flowchart TD
A[Request] --> B[Assign request id, open access log span]
B --> C[Authenticate: virtual key, MCP OAuth JWT, or service JWT]
C --> D[Load pinned workspace + policy snapshot pair]
D --> E{Frozen? Env mismatch? Missing scope?}
E -->|yes| X[Error envelope with x-request-id]
E -->|no| F{Route}
F -->|/v1/*| G[Parse request, CEL model visibility]
G --> H[Resolve model target and fallbacks]
H --> I[Cedar 'complete' on the model]
I --> J[Admission: RPM, budget, token reservation]
J --> K[SSRF guard on the upstream URL]
K --> L[Vault unwrap, render request, send with UsageGuard]
L --> M[Passthrough JSON or SSE]
F -->|/mcp| N[Verify optional approval token headers]
N --> O[MCP server: session, JSON-RPC method]
O --> P[GatewayHooks: resolve, claim idempotency, CEL, Cedar]
P --> Q{Verdict}
Q -->|deny or unknown| Y[Unknown tool]
Q -->|approval required| Z[approval_required + Approval ticket]
Q -->|allow| R[Vault unwrap, dispatch, verify effect]
R --> S[Commit receipt + verification event, then respond]
```
## What the gateway proves
The guarantees the gateway makes *before* a call are the same everywhere: the
key is authenticated, the tool or model is visible to it under CEL, Cedar
allowed the action under the pinned policy publication, admission limits held,
and an idempotency key was assigned.
What the gateway can *prove after* the call is not uniform. A native route
executes through the gateway's own OpenAPI executor, so the receipt can record a
gateway-observed outcome. A federated catalog route executes at a broker the
gateway did not observe, so its evidence is `untrusted` or `broker_attested` and
never `verified`. The receipt always names which class it is — read
[Receipts](/receipts/overview) before relying on one.
## Error envelopes
Every error is one `ErrorEnvelope` rendered in the client's dialect. `/v1/*`
OpenAI-format routes get an `error` object with `message`, `type`, `code`, and
`param`; `/v1/messages` gets the Anthropic `type: "error"` shape; `/mcp` gets a
JSON-RPC error object carrying the envelope as `error.data`. Every error
response carries `x-request-id`, and `retry-after` when the code supplies one.
## Configuration
Configuration is environment-only, parsed once at startup. `Debug` redacts every
secret.
| Variable | Default | Effect |
|---|---|---|
| `OBOL_LISTEN_ADDR` | `0.0.0.0:8080` | Data-plane bind address |
| `OBOL_METRICS_ADDR` | `0.0.0.0:9091` | Separate metrics/health listener |
| `OBOL_ENV` | `dev` | Environment name; also the default for private-upstream policy |
| `REDIS_URL` | required | Hot-path store; connection is verified at boot |
| `OBOL_CONTROL_URL` | unset | Control plane, for snapshot fallback |
| `OBOL_CONNECTORS_URL` | `http://connectors:8090` | Trusted worker base URL. The binary still carries this default, but **no deployment target sets it and nothing listens there** — there is no worker deployable ([Trusted workers](/connectors/trusted-workers)) |
| `OBOL_SERVICE_JWT_SECRET` | required | Shared secret for gateway/control service JWTs |
| `OBOL_KEY_TTL_S` | `60` | Cached `KeyContext` TTL |
| `OBOL_SESSION_TTL_S` | `1800` | MCP session idle TTL |
| `OBOL_IDEMPOTENCY_TTL_S` | `86400` | Invocation record retention |
| `OBOL_UPSTREAM_TIMEOUT_S` | `120` | Per-upstream call timeout |
| `OBOL_DRAIN_TIMEOUT_S` | `30` | Total graceful-shutdown budget |
| `OBOL_ALLOW_PRIVATE_UPSTREAMS` | `true` only when `OBOL_ENV=dev` | Loopback/private upstream guard, see [egress and SSRF](/security/egress-and-ssrf) |
`OBOL_IDEMPOTENCY_TTL_S` is validated at startup: it must exceed the upstream
plus drain timeouts, and must not exceed the store maximum. A bad value fails
the process at boot rather than at the first call.
## Startup and drain
`run` connects Redis and fails fast if it is unreachable, preloads up to 1000
workspace snapshots, spawns the invalidation subscriber, then binds. Shutdown on
`SIGTERM` or `SIGINT` flips `/readyz` and `/startupz` to draining, stops accepting connections,
waits for in-flight requests and metering reconciliation, and closes the meter
queue — all inside one `OBOL_DRAIN_TIMEOUT_S` deadline. See
[Sessions and drain](/gateway/sessions).
## Metrics
The gateway registers these Prometheus series:
| Metric | Labels |
|---|---|
| `obol_requests_total` | route, status |
| `obol_mcp_requests_total` | JSON-RPC method, status |
| `obol_llm_tokens_total` | model, direction |
| `obol_cost_micros_total` | — |
| `obol_policy_decisions_total` | action |
| `obol_meter_dropped_total` | — |
| `obol_invocation_finalize_failures_total` | — |
| `obol_snapshot_loaded` | — |
JSON-RPC method labels are drawn from a fixed allowlist; an unrecognized method
name is labeled `unknown` so an attacker cannot grow the metric cardinality. See
[Telemetry](/observability/telemetry).
## Next
The OpenAI-compatible surface, translation, streaming, and error mapping.
Tool namespacing, list filtering, invocation, and approvals.
Opaque session ids, snapshot consumption, graceful shutdown.
What every crate in `apps/gateway/crates` is responsible for.