Chelabase

Gateway

The front door: authentication, routing, CORS, limits and the internal identity token.

The gateway is a Pingora proxy on 127.0.0.1:8000 by default (CHELABASE_GATEWAY_LISTEN takes a list, e.g. 127.0.0.1:8000,[::1]:8000). It routes by host name: the API host serves the APIs, and the Studio, docs, landing and mail hosts are passed through to their apps. The auth host serves nothing (every path is 404): it only names the issuer of member tokens. All API traffic goes through it: the Management API (/v1/*), the login API (/auth/v1/*) and the data API (/data/v1/*). It never reads request bodies.

One declaration per endpoint

Every public endpoint is declared once, in the chelabase-routes crate: method, path, owning service, who may call it and which origins may use it from a browser. The gateway routes and applies policy from that list; the owning service builds its axum routes from the same list, so the two cannot drift. The OpenAPI tests also check each document against it (see API generation).

Each request goes through a fixed pipeline: request id, host scope, route, CORS, authentication, internal identity. Nothing is configurable per route except the declared policy.

PolicyMeaning
PublicNo credential (for example GET /health, the sign-in routes, the API reference)
ManagementA member access token (JWT)
Data keyA data API key of either kind (chk_pub_ or chk_sec_)
Secret data keyOnly a secret key (GET /data/v1/schema)
Public data keyOnly a public key (the app-user sign-in flows); a secret key is refused

Management API requests

Management endpoints require Authorization: Bearer <member access token>, the EdDSA JWT that auth signs at POST /auth/v1/token (see Identity service). The gateway verifies it locally with the member public key (CHELABASE_MEMBER_JWT_PUBLIC_KEY): the algorithm, key id, issuer (derived from CHELABASE_AUTH_HOST and CHELABASE_PUBLIC_SCHEME; see Hosts), audience chelabase-member, claim set and lifetime are all pinned. Without a verify key every bearer is a 401 and the gateway reports itself not ready. It then forwards the request with an internal identity token in the x-chelabase-identity header:

  • an EdDSA JWT, valid for 60 seconds, signed for exactly one target service (its aud is orchestrator, auth, rest or realtime);
  • the gateway never forwards Authorization, the Chelabase-User header or a client-supplied x-chelabase-identity.

The internal token carries the member's tenant (the member token's tid). The orchestrator trusts only that token (it holds the verify key, CHELABASE_INTERNAL_VERIFY_KEY) and still requires an active membership in that tenant on every request. Calling the orchestrator's port :8081 directly returns 401.

init-secrets (or just workbench up on a workbench stack) creates the gateway's key pair once; it is never overwritten.

Data API requests

A data API request carries Authorization: Bearer <key>. The gateway:

  1. hashes the key (SHA-256) and looks the hash up in a cache (30 seconds; unknown keys are cached for 30 seconds too);
  2. on a miss, asks the orchestrator's internal listener for the key's branch, role, project and database allow-list;
  3. if the request carries Chelabase-User: <access token> (an app user), verifies that token with the app-user public key and checks that its session is still live (cached 30 seconds). A user token is refused with a secret key (400 user_token_with_secret_key), and the request then runs as authenticated with the user's claims; an invalid, expired or revoked token is a 401, never a fall back to the key's own role;
  4. mints an identity token for rest that carries the branch, key id, role, claims, project and database allow-list.

rest enforces the allow-list, because the gateway never reads bodies. A revoked key stops working within 30 seconds, the cache lifetime.

Cache misses are rate limited to 20 per second per client address, and a request over the limit gets 429 rate_limited. The limiter keys on the connection's address, or on the client address forwarded by a proxy listed in CHELABASE_GATEWAY_TRUSTED_PROXIES; it ignores X-Forwarded-For from anyone else, and groups IPv6 clients by their /64 prefix. It remembers at most 100,000 addresses (idle ones are forgotten after 60 seconds); past that it evicts entries, and an evicted address starts again with a full allowance. Behind a load balancer every client shares one address.

The gateway answers its own 404 and 405 under /data/ with the data API's error body (code invalid_query), so a client sees one error shape for the whole prefix.

Forwarding and CORS

  • The gateway sets X-Forwarded-For to the client's address (never trust it for authorization), X-Forwarded-Host to the request's host, and X-Forwarded-Proto to CHELABASE_PUBLIC_SCHEME. It speaks plain HTTP itself.
  • Studio endpoints allow Studio's origin (from CHELABASE_STUDIO_HOST); the sign-in routes also allow cookies for it. The data API endpoints allow any origin (Access-Control-Allow-Origin: *, never with credentials), so browsers can call them with a public key.
  • Only GET /v1/realtime/socket takes a protocol upgrade, and only Upgrade: websocket; its socket may sit idle up to an hour both ways. Any other API request that asks for an upgrade gets 400 invalid_request before it reaches an upstream.
  • A host the gateway does not serve gets a 404 with an empty body, and a request without a Host a 400 with an empty body: nothing in the answer names the gateway or its hosts.

Health

GET /health is public and answered by the orchestrator. GET /_gateway/health is the gateway's own liveness check, answered before any host or route check.

Settings

VariableDefaultMeaning
CHELABASE_GATEWAY_LISTEN127.0.0.1:8000Comma-separated listen addresses
CHELABASE_ORCHESTRATOR_URLhttp://127.0.0.1:8081Management API upstream
CHELABASE_AUTH_URLhttp://127.0.0.1:8084Login API upstream
CHELABASE_REST_URLhttp://127.0.0.1:8085Data API upstream
CHELABASE_REALTIME_URLhttp://127.0.0.1:8087Realtime upstream (tickets and the websocket)
CHELABASE_API_HOST, CHELABASE_STUDIO_HOST, CHELABASE_AUTH_HOSTnone (required)The hosts that serve the API and Studio, and the one that names the member tokens' issuer
CHELABASE_DOCS_HOST, CHELABASE_LANDING_HOST, CHELABASE_MAIL_HOSTunsetOptional hosts for the docs site, the landing page and Mailpit
CHELABASE_PUBLIC_SCHEMEhttpsThe scheme clients use; derives Studio's origin and the issuer
CHELABASE_GATEWAY_TRUSTED_PROXIESnoneReverse proxies whose forwarded client address is believed: CIDRs or addresses, comma-separated
CHELABASE_GATEWAY_UPSTREAM_TIMEOUT_SECS60Upstream read timeout
CHELABASE_INTERNAL_URLhttp://127.0.0.1:8083The orchestrator's internal listener, for key lookups
CHELABASE_DATA_KEYS_TOKENchelabase-dev-data-keysBearer for key lookups
CHELABASE_APP_SESSIONS_INTERNAL_TOKENchelabase-dev-app-sessionsBearer for app-user session liveness
CHELABASE_MEMBER_JWT_PUBLIC_KEYunsetMember token verify key (a PEM file, auth/member-jwt.pub); unset, every bearer is refused
CHELABASE_APP_JWT_PUBLIC_KEYunsetApp-user token verify key (a PEM file); unset, every Chelabase-User header is refused
CHELABASE_INTERNAL_SIGNING_KEYnone (required)The key that signs identity tokens (gateway/internal-signing.pem)

The development bearer values (chelabase-dev-…) are used only with CHELABASE_DEV_DEFAULTS=1 (the workbench); otherwise an unset or development bearer stops the service at start-up. Each bearer can also be given as a file through <NAME>_FILE.

On this page