Hosts
The three host names Chelabase uses (Studio, the API and the token issuer), the DNS and TLS they need, and the rules the gateway checks at startup.
Everything a browser or client reaches goes through the gateway, and the gateway tells the services apart by the Host header. You give Chelabase three host names. There is no path prefix to configure.
| Host | Setting | What it serves |
|---|---|---|
| Studio | CHELABASE_STUDIO_HOST | The Studio web app |
| API | CHELABASE_API_HOST | The management API, the data API and the app-user sign-in routes |
| Auth | CHELABASE_AUTH_HOST | Nothing (every path is 404): it names the issuer of member access tokens |
Three more hosts are optional and serve only when set: CHELABASE_DOCS_HOST (the docs site), CHELABASE_LANDING_HOST (the landing page) and CHELABASE_MAIL_HOST (Mailpit, for local email). A blank value means unset.
For example, on acme.com:
CHELABASE_PUBLIC_SCHEME=https
CHELABASE_STUDIO_HOST=studio.acme.com
CHELABASE_API_HOST=api.acme.com
CHELABASE_AUTH_HOST=auth.acme.comWhy there is an auth host
Member access tokens name their issuer, and Chelabase derives it from the auth host and the scheme (https://auth.acme.com). auth signs with that issuer, and the gateway, the orchestrator and realtime accept only it, so a token minted for one install never passes at another. The sign-in pages themselves are Studio's, and the login API is on the API host. You never set the issuer yourself.
DNS
Point the hosts at the machine that runs the gateway. Either one wildcard record (*.acme.com) or one record per host (studio and api, plus any optional ones) works. The auth host needs no record: nothing is fetched from it. Nothing else needs a record: the gateway is the only public entry.
TLS
The gateway listens in plain HTTP. For HTTPS, terminate TLS in front of it, either with your own reverse proxy or a load balancer. Whoever terminates TLS, set the scheme:
CHELABASE_PUBLIC_SCHEME=httpsCHELABASE_PUBLIC_SCHEME is http or https (case does not matter) and defaults to https; a blank value is https too. It decides the scheme of every URL Chelabase builds (Studio's origin, the API base, the issuer) and the X-Forwarded-Proto the gateway sends to the services. Session cookies are marked Secure exactly when it is https, in every mode; CHELABASE_AUTH_SECURE_COOKIES overrides that, and auth logs a warning at startup when it turns Secure off under https. Use http only for local work: platform mode refuses to start with it.
Calls inside the network
With TLS in front, the public URLs only work from outside: inside the network nothing speaks TLS. The services never call them. Member tokens are verified locally with the member public key, so no service fetches keys, and the services reach each other at their internal addresses (the identity service at http://identity:8080).
Behind a reverse proxy
By default the gateway takes the client's address from the TCP connection and sets X-Forwarded-For to that address alone, replacing whatever the client sent. Behind a reverse proxy that address is the proxy's, so every client shares one per-IP bucket: sign-up is limited to 20 an hour per client address, and one noisy client could use up the hour's sign-ups for everybody, including the setup-code claim.
Tell the gateway which proxies to believe:
# The address the gateway sees your proxy at. For a proxy on the same machine, the "Gateway"
# address that `docker network inspect <project>_default` prints (the project is `compose`
# unless you named it).
CHELABASE_GATEWAY_TRUSTED_PROXIES=172.18.0.1It takes a comma-separated list of CIDRs (10.0.0.0/8, fd00::/8) or single addresses (192.168.1.5), written as the gateway sees the proxy. When a request comes from one of them, the gateway reads the proxy's X-Forwarded-For (or Forwarded, its for=), walks it from right to left past the trusted addresses, and takes the first one that isn't trusted as the client. Requests from anyone else are handled as before, so a forged header from a client that reaches the gateway directly changes nothing. The services still get exactly one address in X-Forwarded-For.
- Set your proxy to add the client's address, and to clear the other header. nginx:
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;. Caddy and Traefik add it on their own. Use one of the two headers, and have the proxy remove the one you don't use (nginx:proxy_set_header Forwarded "";): most proxies, nginx included, pass a client's ownForwardedthrough unchanged. - Which address to list. With Compose and a proxy on the same machine connecting to the published port, the gateway sees the Docker network's gateway address rather than
127.0.0.1;docker network inspect <project>_defaultshows it. List that one address. A range such as172.16.0.0/12would also cover it, but it trusts every container on every Docker network of the host, and any of them could then set any client's address; use it only if the address keeps changing, and know what it lets in. A proxy on another machine is seen at its own address. - List only what can't be forged. Anything listed can name any client address. Never list a range your users' traffic arrives from directly; a prefix of
/0is refused. On Docker Desktop and with rootless Docker, every connection to a published port can appear to come from one internal address, whoever made it. If you trust that address while the port is published beyond loopback, clients that connect to the port directly can forgeX-Forwarded-For: keepCHELABASE_HTTP_ADDR=127.0.0.1there, so only your proxy on the same machine reaches the gateway. - When the header can't be trusted, the gateway falls back to the proxy's address. That happens when a trusted proxy sends both headers or neither, a header over 4096 bytes or 32 hops, or an entry the walk reaches that isn't an address (
unknown, an obfuscated name). The request is never refused for it. A client behind the proxy can force this fallback on purpose: by sending its ownForwardedheader (so the gateway sees both), or by paddingX-Forwarded-Forpast 32 hops or 4096 bytes before the proxy appends to it. It then shares the proxy's bucket with every other request that fell back, and can use up that bucket's sign-ups and sign-ins. Clearing the header you don't use at the proxy (above) closes the first way. When your proxy is the first one clients reach, closing the second means it replaces the header with the address it sees instead of appending to it (nginx:proxy_set_header X-Forwarded-For $remote_addr;). - Keep
CHELABASE_AUTH_TRUSTED_PROXY=gatewayas the install sets it. Auth then believes theX-Forwarded-Forthe gateway sets and nobody else's.
An invalid entry stops the gateway at startup with a message naming CHELABASE_GATEWAY_TRUSTED_PROXIES.
Settings
| Setting | Default | What it is |
|---|---|---|
CHELABASE_STUDIO_HOST, CHELABASE_API_HOST, CHELABASE_AUTH_HOST | none (required) | The three hosts, as name or name:port |
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 | http or https (platform mode requires https) |
CHELABASE_GATEWAY_LISTEN | 127.0.0.1:8000 | Where the gateway listens: a comma-separated list of ip:port addresses, IPv6 in brackets (127.0.0.1:8000,[::1]:8000) |
CHELABASE_GATEWAY_TRUSTED_PROXIES | none | Reverse proxies whose forwarded client address the gateway believes: CIDRs or addresses, comma-separated (see Behind a reverse proxy) |
CHELABASE_STUDIO_UPSTREAM | http://127.0.0.1:3000 | Where the gateway reaches Studio |
CHELABASE_DOCS_UPSTREAM | http://127.0.0.1:3001 | The docs site (used only with a docs host) |
CHELABASE_LANDING_UPSTREAM | http://127.0.0.1:3002 | The landing page (used only with a landing host) |
CHELABASE_MAIL_UPSTREAM | http://127.0.0.1:8025 | Mailpit (used only with a mail host) |
These are the services' own defaults. The Compose install differs: it defaults the scheme to http, sets the gateway's listen address and the Studio upstream itself, and doesn't pass the optional docs, landing and mail hosts, so setting those in .env has no effect there.
The gateway also reaches the orchestrator, auth, the data API engine and realtime through CHELABASE_ORCHESTRATOR_URL, CHELABASE_AUTH_URL, CHELABASE_REST_URL and CHELABASE_REALTIME_URL.
Rules checked at startup
The gateway and every service that reads the hosts refuse to start, with a message naming the setting, when any of these fail:
- Studio and the API are one site: they share a parent domain of at least two labels.
studio.acme.comandapi.acme.compass, and so dostudio.acme.pages.devandapi.acme.pages.dev(one site underacme.pages.dev); two hosts on different domains don't. The sign-in cookies areSameSite=Strict, so the browser sends them from Studio to the API only when both are the same site. The check has no public-suffix list, so it can't catch two hosts directly under a public suffix:a.pages.devandb.pages.devpass it, yet they are two sites and sign-in fails. Pick a domain you own. If both hosts are IP addresses, they must be the same address. - No host carries its scheme's default port. Write
studio.acme.com, notstudio.acme.com:443: browsers drop the default port fromHostandOrigin, so the setting could never match. - A port in a setting matches only that port.
studio.chelabase.localhost:8000serves requests whoseHostis exactly that. A host without a port matches any port. When both match, the one with the port wins. - No two hosts are identical (after lowercasing and dropping a trailing dot).
- The issuer is derived from the auth host and the scheme.
- Platform mode requires
https. WithCHELABASE_MODE=platform, the gateway, auth and the orchestrator refuseCHELABASE_PUBLIC_SCHEME=http.
Settings that were removed
| Removed | Use instead |
|---|---|
CHELABASE_API_HOSTS | CHELABASE_API_HOST (one host) |
CHELABASE_STUDIO_ORIGINS | CHELABASE_STUDIO_HOST; the allowed CORS origin is derived from it |
CHELABASE_STUDIO_URL | CHELABASE_STUDIO_HOST |
CHELABASE_GATEWAY_ADDR | CHELABASE_GATEWAY_LISTEN |
A service that finds one of the removed settings set refuses to start and names its replacement.
Install
Self-host Chelabase with Docker Compose, claim the instance with its setup code, choose your own secrets, and upgrade.
Example organisation
Start a local Chelabase stack with a demo organisation, forty people in five teams, schemas, row-level security, app users and a branch to merge, and what to try in it.