Operations
Once the gateway is serving traffic, day-to-day operation is reading its logs, probing its health, and rotating its secrets on your schedule. The subsections cover each, plus what Postgres holds and how upgrades and rollbacks behave.
Logs
The gateway writes two streams to stderr, both JSON-friendly:
- Audit events: single-line JSON per security-relevant event. Pipe stderr to your log aggregator. The events emitted include
config.load,session.mint,session.refresh,device.authorize,device.verify,device.callback,auth.denied,access.denied,inference,managed.serve,desktop_bootstrap.serve,desktop_bootstrap.denied,spend.blocked,admin.denied,admin.limit.upsert, andadmin.limit.delete. Fields vary by event:- Successful mint and refresh events carry
sub,email,client_ip, and the result auth.deniedandaccess.deniedcarry the reason and client IP, plus the request path forauth.denied, since no user identity exists at those denials. Twoaccess.deniedreasons change what the event carries:xff_unparseable: the event also carries theX-Forwarded-Forentry that couldn't be readclient_ip_unknown: the event carries no client IP, because the connection had no peer address while anaccess_controllist was set
inferencerecords which upstream served the request and the response statusdesktop_bootstrap.deniedrecords a rejected Claude Desktop bootstrap fetch with the reason (not_configured,policy_not_opted_in, orno_policy_matched) and the user's identityadmin.deniedrecords a rejected admin-API auth attempt with the client IP, method, path, and a reason, without the presented key material:invalid_keywhen anx-api-keywas presented but matched no configured key,bearer_rejectedwhen only anAuthorizationheader was presented and it didn't verify as a gateway session inadmin.admin_groups, orno_credentialswhen neither header was presented
- Successful mint and refresh events carry
- Operational logs: human-readable
[gateway]-prefixed lines for boot, warnings, and upstream errors. TheCLAUDE_GATEWAY_LOG_LEVELenvironment variable controls verbosity and acceptsdebug,info,warn, orerror, withinfoas the default. Atdebug, each sign-in and refresh also logs the names, not the values, of the claims in the id_token, plus the names of the userinfo claims whenuserinfo_fallbacksupplied any, so you can diagnoseemail_claimandgroups_claimsettings without logging PII. It doesn't affect audit events, which are always emitted.
Health
The gateway serves GET /healthz as a liveness probe and GET /readyz as a readiness probe; /readyz verifies the store is reachable. Both are exempt from access_control.allow_cidrs, so probes keep working on a locked-down listener.
The OAuth discovery document at /.well-known/oauth-authorization-server also returns 200 only after config load, OIDC discovery, upstream client construction, and Postgres migration all succeed, so it doubles as an end-to-end boot check.
Outage behavior
If Postgres goes down, the gateway itself keeps serving signed-in developers and new sign-ins fail. Whether developers actually keep working depends on how your orchestrator handles readiness:
- Existing sessions: bearer tokens validate locally with the JWT secret, session refreshes don't touch the store, and the gateway process can still serve inference
- New sign-ins: fail until Postgres recovers, because the device flow and its rate-limit counters live in Postgres
- Spend-limit enforcement: fails open by default during the outage, so inference still flows; flip it to fail closed if you'd rather block than run unmetered
- Readiness:
/readyzreports not-ready during the outage, so orchestrators that gate traffic on readiness remove every replica from rotation at once. In that topology all traffic, including inference the gateway could still serve, fails at the load balancer until Postgres recovers. The liveness probe on/healthzkeeps passing, so replicas aren't restarted. Point the readiness probe at/healthzinstead if you'd rather signed-in developers keep working through a store outage; the cost is that new sign-ins fail against a replica that still reports ready.
If your IdP goes down, existing sessions work until ttl_hours, and new logins and refreshes fail. Set a longer ttl_hours if your IdP has frequent maintenance windows.
JWT secret rotation
Rotate the signing secret in three steps so existing sessions stay valid:
- Generate a new secret. Prepend it to the
session.jwt_secretarray. - Roll the deployment. New tokens sign with the new secret; old tokens still verify.
- After
ttl_hoursplus a margin, remove the old secret and roll again.
Rotation is also the only way to force sessions out before they expire: bearer tokens validate locally against the JWT secret, so there is no per-session revocation. Replacing the secret outright, without keeping the old one in the array, invalidates every outstanding session at once. For individual offboarding, deprovision the user in your IdP; their session ends within ttl_hours.
Postgres
The gateway holds five data tables plus a _migrations table, all created by its boot-time migrations:
| Table | Contents | Retention |
|---|---|---|
kv | Device grants (10-minute TTL) and rate-limit counters | TTL per row |
spend | Per-principal period-to-date spend counters, in cents | admin.spend_retention_months, default 13 |
spend_limits | Configured spend caps | Until deleted via the API |
admin_audit | Admin API mutation trail | admin.audit_retention_days, default 365 |
principal_emails | Each principal's last-seen email, display name, and IdP groups. Contains PII. | admin.identity_retention_days since last activity, default 90 |
A 30-second loop expires kv rows past their TTL, and an hourly sweep enforces the retention windows on the spend tables, so nothing grows without bound. Without spend limits configured, only kv is written. The gateway applies its own schema migrations at boot and on every upgrade, so its database role needs rights to create and alter tables. Point it at a database or schema dedicated to the gateway to keep that grant narrow.
With spend limits in use, a lost database means lost spend tracking and caps, not only developer re-logins, so run regular backups. To erase one departed developer immediately rather than waiting on retention, run DELETE FROM principal_emails WHERE principal = '<sub>' directly; that removes the only table holding their email, name, and groups. spend and admin_audit rows reference the pseudonymous OIDC sub only.
Upgrades
Replicas are stateless, so a rolling restart is safe at any time. The gateway runs schema migrations at boot, which means deploying the new binary self-migrates the database. Concurrent replicas serialize on a Postgres advisory lock, so only one applies each migration.
Migrations are append-only, so rolling back to a prior binary that knows fewer migrations is safe; it ignores the extra rows. Rollback also re-validates the YAML against the older binary's schema, so a config that adopted a key introduced by the newer release fails boot on the older one. Remove the new key before rolling back.
Because you pin the gateway's version in your own image, fixes in new Claude Code releases, including security fixes, reach your deployment only when you update the pin and redeploy. Include the gateway in the same patching cadence you use for other services that hold production credentials.