Chelabase

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.

HostSettingWhat it serves
StudioCHELABASE_STUDIO_HOSTThe Studio web app
APICHELABASE_API_HOSTThe management API, the data API and the app-user sign-in routes
AuthCHELABASE_AUTH_HOSTNothing (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.com

Why 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=https

CHELABASE_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.1

It 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 own Forwarded through 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>_default shows it. List that one address. A range such as 172.16.0.0/12 would 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 /0 is 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 forge X-Forwarded-For: keep CHELABASE_HTTP_ADDR=127.0.0.1 there, 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 own Forwarded header (so the gateway sees both), or by padding X-Forwarded-For past 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=gateway as the install sets it. Auth then believes the X-Forwarded-For the gateway sets and nobody else's.

An invalid entry stops the gateway at startup with a message naming CHELABASE_GATEWAY_TRUSTED_PROXIES.

Settings

SettingDefaultWhat it is
CHELABASE_STUDIO_HOST, CHELABASE_API_HOST, CHELABASE_AUTH_HOSTnone (required)The three hosts, as name or name:port
CHELABASE_DOCS_HOST, CHELABASE_LANDING_HOST, CHELABASE_MAIL_HOSTunsetOptional hosts for the docs site, the landing page and Mailpit
CHELABASE_PUBLIC_SCHEMEhttpshttp or https (platform mode requires https)
CHELABASE_GATEWAY_LISTEN127.0.0.1:8000Where the gateway listens: a comma-separated list of ip:port addresses, IPv6 in brackets (127.0.0.1:8000,[::1]:8000)
CHELABASE_GATEWAY_TRUSTED_PROXIESnoneReverse proxies whose forwarded client address the gateway believes: CIDRs or addresses, comma-separated (see Behind a reverse proxy)
CHELABASE_STUDIO_UPSTREAMhttp://127.0.0.1:3000Where the gateway reaches Studio
CHELABASE_DOCS_UPSTREAMhttp://127.0.0.1:3001The docs site (used only with a docs host)
CHELABASE_LANDING_UPSTREAMhttp://127.0.0.1:3002The landing page (used only with a landing host)
CHELABASE_MAIL_UPSTREAMhttp://127.0.0.1:8025Mailpit (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.com and api.acme.com pass, and so do studio.acme.pages.dev and api.acme.pages.dev (one site under acme.pages.dev); two hosts on different domains don't. The sign-in cookies are SameSite=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.dev and b.pages.dev pass 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, not studio.acme.com:443: browsers drop the default port from Host and Origin, so the setting could never match.
  • A port in a setting matches only that port. studio.chelabase.localhost:8000 serves requests whose Host is 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. With CHELABASE_MODE=platform, the gateway, auth and the orchestrator refuse CHELABASE_PUBLIC_SCHEME=http.

Settings that were removed

RemovedUse instead
CHELABASE_API_HOSTSCHELABASE_API_HOST (one host)
CHELABASE_STUDIO_ORIGINSCHELABASE_STUDIO_HOST; the allowed CORS origin is derived from it
CHELABASE_STUDIO_URLCHELABASE_STUDIO_HOST
CHELABASE_GATEWAY_ADDRCHELABASE_GATEWAY_LISTEN

A service that finds one of the removed settings set refuses to start and names its replacement.

On this page