# Hosts (/docs/self-hosting/hosts)



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`:

```bash
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 [#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 [#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 [#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:

```bash
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 [#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 [#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:

```bash
# 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 [#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](#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 [#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 [#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.
