# Gateway (/architecture/gateway)



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 [#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](/architecture/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.

| Policy          | Meaning                                                                          |
| --------------- | -------------------------------------------------------------------------------- |
| Public          | No credential (for example `GET /health`, the sign-in routes, the API reference) |
| Management      | A member access token (JWT)                                                      |
| Data key        | A data API key of either kind (`chk_pub_` or `chk_sec_`)                         |
| Secret data key | Only a secret key (`GET /data/v1/schema`)                                        |
| Public data key | Only a public key (the app-user sign-in flows); a secret key is refused          |

## Management API requests [#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](/architecture/identity#member-sign-in-and-tokens)). 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](/self-hosting/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 [#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 [#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 [#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 [#settings]

| Variable                                                               | Default                      | Meaning                                                                                         |
| ---------------------------------------------------------------------- | ---------------------------- | ----------------------------------------------------------------------------------------------- |
| `CHELABASE_GATEWAY_LISTEN`                                             | `127.0.0.1:8000`             | Comma-separated listen addresses                                                                |
| `CHELABASE_ORCHESTRATOR_URL`                                           | `http://127.0.0.1:8081`      | Management API upstream                                                                         |
| `CHELABASE_AUTH_URL`                                                   | `http://127.0.0.1:8084`      | Login API upstream                                                                              |
| `CHELABASE_REST_URL`                                                   | `http://127.0.0.1:8085`      | Data API upstream                                                                               |
| `CHELABASE_REALTIME_URL`                                               | `http://127.0.0.1:8087`      | Realtime upstream (tickets and the websocket)                                                   |
| `CHELABASE_API_HOST`, `CHELABASE_STUDIO_HOST`, `CHELABASE_AUTH_HOST`   | none (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_HOST` | unset                        | Optional hosts for the docs site, the landing page and Mailpit                                  |
| `CHELABASE_PUBLIC_SCHEME`                                              | `https`                      | The scheme clients use; derives Studio's origin and the issuer                                  |
| `CHELABASE_GATEWAY_TRUSTED_PROXIES`                                    | none                         | Reverse proxies whose forwarded client address is believed: CIDRs or addresses, comma-separated |
| `CHELABASE_GATEWAY_UPSTREAM_TIMEOUT_SECS`                              | `60`                         | Upstream read timeout                                                                           |
| `CHELABASE_INTERNAL_URL`                                               | `http://127.0.0.1:8083`      | The orchestrator's internal listener, for key lookups                                           |
| `CHELABASE_DATA_KEYS_TOKEN`                                            | `chelabase-dev-data-keys`    | Bearer for key lookups                                                                          |
| `CHELABASE_APP_SESSIONS_INTERNAL_TOKEN`                                | `chelabase-dev-app-sessions` | Bearer for app-user session liveness                                                            |
| `CHELABASE_MEMBER_JWT_PUBLIC_KEY`                                      | unset                        | Member token verify key (a PEM file, `auth/member-jwt.pub`); unset, every bearer is refused     |
| `CHELABASE_APP_JWT_PUBLIC_KEY`                                         | unset                        | App-user token verify key (a PEM file); unset, every `Chelabase-User` header is refused         |
| `CHELABASE_INTERNAL_SIGNING_KEY`                                       | none (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`.
