02 β Architecture
Componentsβ
| Component | Role |
|---|---|
| Gateway binary | Rust process. Speaks MCP to agents over HTTP/SSE. Holds DB credentials, enforces permissions, signs tokens, writes audit logs. |
| Gateway state DB | A small Postgres co-deployed with the gateway. Stores: SSO sessions, audit logs (hot window), per-user query stats. Not the customer's DB. |
| Target databases | The customer's actual databases (prod, staging, analyticsβ¦). Gateway connects with a per-database read-only role. |
| Identity provider (SSO) | Existing OIDC IdP (Okta, Google Workspace, Authentik, Keycloakβ¦). Source of truth for users and group memberships. |
| Config file | YAML in git. Defines servers, databases, groups, permissions, retention, SSO settings. |
Request pathβ
βββββββββββββββ MCP/HTTP+SSE βββββββββββββββββββββββ pg wire ββββββββββββββββ
β Agent β βββββββββββββββββββΆ β Gateway (Rust) β βββββββββββββΆ β Target DB β
β (Claude β Bearer: <sso-jwt> β β role=ro_user β (read-only β
β Code, β¦) β βββββββββββββββββββ β βββββββββββββββββ β βββββββββββββ β role) β
βββββββββββββββ tool result JSON β β authz + audit β β result rows ββββββββββββββββ
β βββββββββββββββββ β
β β β
β βΌ β
β ββββββββββββ β
β β state DB β β
β β (audit) β β
β ββββββββββββ β
βββββββββββββββββββββββ
Transport (wire protocol)β
The gateway speaks MCP over Streamable HTTP (the 2025-03+ MCP transport), not the deprecated two-endpoint HTTP+SSE transport:
- A single configurable endpoint (default
/mcp). POSTcarries clientβserver JSON-RPC 2.0 messages; the gateway replies with a JSON-RPC response (application/json), or202 Acceptedfor notifications.GETopens the serverβclient SSE stream for server-initiated messages. On connect it emits a non-normativegreetingevent (protocol version + server identity) so a plaincurlconfirms liveness, then holds open with keep-alives.
Protocol version: 2025-06-18. The framing is hand-rolled JSON-RPC; it gets swapped for an official MCP server SDK when one stabilizes (see 11-roadmap). Transport owns framing only β auth, tool dispatch, and audit are separate layers below.
Layers inside the gatewayβ
One reason to change per layer. Don't blur these.
| Layer | Owns | Why it's its own thing |
|---|---|---|
| Transport | MCP over HTTP+SSE, JSON-RPC framing, capability negotiation | Protocol churns; isolate it |
| Auth | OIDC flow, JWT verification, session cache, group resolution | Security boundary; must be auditable in isolation |
| Authz | Map (user, groups) Γ (server, database, action) β allow/deny + constraints | Pure function over config; testable without a DB |
| Tool dispatch | The MCP tool surface: list_databases, describe_schema, sample_table, run_query, explain | Stable contract to agents |
| Query exec | Connection pool per DB, statement timeout, row cap, cancellation | Where most of the safety lives |
| Audit | Append-only writes to state DB, structured logs, retention pruner | Append-only, never read by hot path |
| Config | Load + validate YAML, hot reload on SIGHUP, secrets resolution | Decoupled so config errors fail at boot, not at query time |
Process modelβ
- Single binary, single process, async runtime (tokio).
- Connection pool per target database, sized in config.
- One Postgres pool for the state DB.
- A background task per pool for health checks; one for audit retention pruning; one for SSO key rotation.
Concurrencyβ
The gateway is the shared substrate for an entire engineering org hammering away from multiple agents at once. Every layer is built for that load shape from the start.
- All I/O is async. Tokio runtime, axum HTTP, sqlx async driver. One slow query never blocks another developer's request.
- No per-user serialization. A developer can run multiple agents (Claude Code in one repo, Cursor in another) under the same SSO identity, and they share the session token but not the connection β every request gets a fresh pooled connection.
- Per-database concurrency cap. The pool
max_connectionsis the upper bound. Bursts beyond that queue at the gateway with theacquire_timeoutceiling β agents see a cleanunavailableerror instead of cascading timeouts down to the DB. - Tool dispatch is non-blocking. Each MCP request is its own tokio task. Cancellations (agent disconnect, statement timeout) propagate cleanly down to the DB driver β
pg_cancel_backendon Postgres β so an abandoned query doesn't burn a connection until it finishes on its own. - Audit writes don't queue head-of-line. The audit log writer has its own state-DB pool, separate from session reads, so a burst of queries doesn't starve session validation or vice versa.
- Fairness. When a
(server, database)pool is saturated, waiters are FIFO. We will not implement per-user priority in v1; if one user is dominating prod, the operator's lever is the permissions config (lowerrow_limit, lowerstatement_timeout_ms), not in-gateway scheduling.
What this means in practiceβ
| Scenario | Behavior |
|---|---|
50 agents call list_databases simultaneously | All return in parallel; no target DB hit at all β served from cached metadata |
10 agents run_query against prod/app (pool max 5) | First 5 execute; next 5 queue up to acquire_timeout; queue beyond that returns unavailable |
| One agent runs a slow 25s query | Other agents' queries against the same DB execute in parallel on other pool connections; that agent's own next query waits or fails fast |
| Agent disconnects mid-query | pg_cancel_backend fires, the connection returns to the pool within seconds, audit log records outcome: cancelled |
| 1000 audit writes in 10s | Audit pool absorbs the burst; if it can't, the request fails (synchronous audit invariant β see 01-overview) rather than queueing audit and lying about durability |
HAβ
For organizations that need to survive a single gateway instance going down: run two replicas behind a load balancer. Session state is in the state DB, not in-process, so an established session (a Bearer JWT on /mcp) follows the request to whichever replica picks it up β sticky sessions are not required for the steady-state request path. The only singleton background task β audit retention pruning β uses an advisory lock in the state DB so only one replica runs it at a time.
Session revocation is eventually consistent across replicas. Each replica fronts the state DB with a small in-process session cache that has a freshness TTL (SESSION_CACHE_TTL_SECONDS, default 30s) and a hard size bound. A logout/revoke evicts the entry on the replica that handled it (immediate there), but other replicas keep serving their cached copy until it ages past the TTL and re-validates against revoked_at. The cross-replica revocation window is therefore β€ the TTL β tune it down for faster propagation, up to spend fewer state-DB reads (see 04-auth-sso Β§Session tokens).
One exception: the MCP OAuth bridge. The bridge's short-lived flow state β pending IdP round-trips, one-time authorization codes, and rotating refresh tokens β is in-process, not in the state DB (see 04-auth-sso). A client that begins the login dance (/authorize β /auth/callback β /token) must reach the same replica for every step, or it sees invalid_grant from a replica that never saw the earlier step. Until that state also moves to the state DB (roadmap), an HA deployment must either run the bridge on a single replica or pin the login endpoints (/authorize, /auth/callback, /token) with sticky routing. Dynamic client registrations are the exception's exception: they now live in the state DB (oauth_clients), so /register is replica-agnostic and survives restarts/redeploys β a client that cached its client_id no longer gets invalid_client after a rollout. The bespoke /auth/login JSON flow has the same single-replica/sticky constraint for its login round-trip. This does not affect already-authenticated /mcp traffic.
Statelessnessβ
The gateway process is stateless apart from in-memory caches (JWKS, sessions, decoded permissions) and the MCP OAuth bridge's in-process flow state (pending logins, auth codes, refresh tokens). Client registrations moved to the state DB (oauth_clients), so they persist across restarts. Restarting the binary loses nothing that isn't reloadable from config + state DB β an in-flight OAuth login is the one thing a restart drops, and the client simply retries the login (its registration still resolves). Two replicas behind a load balancer is the path to HA; sticky sessions are not required for the steady-state /mcp request path because session state is in the state DB, but the OAuth bridge login dance needs single-replica or sticky routing (see HA above).
What's deliberately not hereβ
- No query builder, no result UI. Agents are the UX.
- No write-mode default. See 06-permissions for opt-in writes.
- No SaaS control plane. Every install is self-hosted and self-contained.
- No multi-tenant boundary. One install per organization. Tenancy = run another install.