# Workspace subdomains

Workspace subdomains give each workspace an address such as `https://ls1intum.hephaestus.build`. Authentication and the API stay on the apex, `https://hephaestus.build`. The instance switch is off by default. The web app supports workspace addresses. Prepare DNS, TLS, and OAuth callbacks before you turn on the switch.

## Configure the server

The server uses these properties:

| Property                                      | Environment variable                          | Default |
| --------------------------------------------- | --------------------------------------------- | ------- |
| `hephaestus.workspace.subdomains.enabled`     | `HEPHAESTUS_WORKSPACE_SUBDOMAINS_ENABLED`     | `false` |
| `hephaestus.workspace.subdomains.base-domain` | `HEPHAESTUS_WORKSPACE_SUBDOMAINS_BASE_DOMAIN` | Empty   |

The base domain must use lowercase ASCII. It must not contain a scheme, port, or path. Its maximum length is 189 characters, to leave space for a full workspace label. With the switch on, the webapp URL and auth issuer must use the HTTPS apex. The webapp URL must have no final slash.

```
hephaestus:

  workspace:

    subdomains:

      enabled: true

      base-domain: hephaestus.build

  webapp:

    url: https://hephaestus.build

  auth:

    issuer: https://hephaestus.build

    api-base-path: /api
```

The server switch alone does not configure DNS, TLS, redirects, or SPA routes. Use DNS-only records and a DNS-01 wildcard certificate, as [ADR 0053](https://github.com/hephaestus-build/Hephaestus/blob/main/docs/decisions/0053-workspace-addresses-are-a-presentation-origin.md) requires. Cloudflare must not proxy these hosts.

Before you turn it on, configure DNS, TLS, and the apex callbacks. To turn it off, set `enabled` to `false` in the app and proxy settings and recreate both. Keep the apex workspace paths available.

## Configure DNS and the edge

DNS and TLS can be prepared while the switch stays off. If the zone has CAA records, allow `letsencrypt.org` in `issue` for the apex. If it has `issuewild` records, they must also allow `letsencrypt.org` for the wildcard. See [CAA guidance](https://letsencrypt.org/docs/caa/). The edge serves the same trusted SPA on every valid tenant host. It does not check whether a workspace exists.

### Cloudflare checklist for hephaestus.build

1. Open the `hephaestus.build` zone and select **DNS**.
2. Create a wildcard record with **Name** `*`, pointing to the same origin as the apex.
3. Set the wildcard and apex records to **DNS only** (grey cloud).
4. Keep explicit records for reserved services, including `docs` and `staging`, separate from the wildcard.
5. Open **My Profile → API Tokens → Create Token** and select **Edit zone DNS**.
6. Add **Zone → Zone → Read** and **Zone → DNS → Edit** permissions.
7. Set **Zone Resources** to **Include → Specific zone → hephaestus.build**.
8. Save only the raw token in `/etc/hephaestus/dns/token`, with file mode `0600`.

Use one zone-scoped token with both permissions. Do not use the account's global API key. Cloudflare supplies DNS only, not the HTTP connection or TLS certificate. Do not configure its proxy, Origin CA, origin locks, or trusted-proxy address ranges. The server trusts only the actual Traefik ingress address.

See [Cloudflare wildcard records](https://developers.cloudflare.com/dns/manage-dns-records/reference/wildcard-dns-records/) and [API token creation](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/). The permissions follow [lego's Cloudflare provider](https://go-acme.github.io/lego/dns/cloudflare/).

### Helm chart and k3s

Set `workspaceSubdomains.enabled: true` in `site.yaml`. The base domain is `host`. The chart passes both settings to the server and webapp. Prepare DNS-only apex and wildcard records first. The Gateway needs a DNS-01 certificate for both `host` and `*.host`.

The chart adds a wildcard HTTPRoute to the webapp alone. nginx rejects reserved or invalid workspace hosts and the API, auth and webhook paths on tenant hosts. Apex GET and HEAD requests to valid workspace paths return a 308 redirect that keeps the path and query. No workspace lookup controls that redirect. The server still checks every workspace permission.

For the one-host platform, prepare a cert-manager ClusterIssuer with a DNS-01 solver for your zone. Keep its scoped credentials in a private Secret in cert-manager's namespace. Do not put a credential value in `site.yaml`. Use the [cert-manager DNS-01 configuration](https://cert-manager.io/docs/configuration/acme/dns01/) for your DNS provider.

Make the issuer and its credential available when k3s applies the platform manifests. On a fresh host, protected manifests can be placed in k3s's manifests directory before the installer runs. Then add `--workspace-subdomains --dns01-issuer <issuer-name>` to `k3s-install.sh`. The installer requests the apex and wildcard certificate from that issuer. For a test certificate, select a test issuer. The installer's `--staging` flag controls only its built-in HTTP-01 issuer.

On an existing install, update `site.yaml` yourself and prepare the wildcard certificate before the chart upgrade. The installer preserves an existing `site.yaml`. To turn workspace hosts off, set `workspaceSubdomains.enabled: false` and upgrade. Keep the DNS records and certificate while other services need them.

### Compose settings

Use the same settings for the multi-file and single-host Compose stacks. The proxy chooses one ACME challenge through these optional variables:

| Variable                            | Default                    | With subdomains                                             |
| ----------------------------------- | -------------------------- | ----------------------------------------------------------- |
| `ACME_CHALLENGE`                    | `httpchallenge.entrypoint` | `dnschallenge.provider`                                     |
| `ACME_CHALLENGE_VALUE`              | `http`                     | Your lego DNS provider, such as `cloudflare`                |
| `TRAEFIK_DNS_CREDENTIALS_DIRECTORY` | `/etc/hephaestus/dns`      | Absolute path to the proxy-only credential directory        |
| `TRAEFIK_DNS_CREDENTIAL_VARIABLE`   | `CF_DNS_API_TOKEN_FILE`    | Your provider's credential variable with the `_FILE` suffix |
| `LEGO_EXPERIMENTAL_DNS_TCP_ONLY`    | `false`                    | Set `true` only when DNS queries need TCP transport         |

For Cloudflare, create the credential directory with mode `0700`. Put only the raw zone-scoped token in its `token` file, with mode `0600`. Keep the directory outside release trees.

The proxy mounts it read-only at `/run/secrets/dns`. lego reads the token through `CF_DNS_API_TOKEN_FILE=/run/secrets/dns/token`. Compose output and container environment values contain paths, not the token. Never add the token to an environment file, diagnostics, or a release artifact. HTTP-01 deployments need no token.

For another provider, use its name and required variables from [lego's provider list](https://go-acme.github.io/lego/dns/). Choose a provider that supports certificates for both a wildcard and its apex. Set `TRAEFIK_DNS_CREDENTIAL_VARIABLE` to its `_FILE` credential name.

For a provider with multiple credentials, store the other files in the same directory. Add its other `_FILE` variables to the proxy environment through a Compose override. Each value must point to its file under `/run/secrets/dns`. See [lego credential files](https://go-acme.github.io/lego/dns/#configuration-and-credentials). Do not disable DNS propagation checks. Allow the proxy to reach the provider API and public DNS servers.

If authoritative DNS queries time out over UDP while TCP works, set `LEGO_EXPERIMENTAL_DNS_TCP_ONLY=true` in the proxy settings and recreate the proxy. This uses [lego's TCP transport](https://github.com/go-acme/lego/blob/v5.5.2/challenge/internal/client.go) for DNS queries, including propagation checks. It leaves the existing propagation-check settings unchanged. The default is `false`. Set it back to `false` and recreate the proxy to restore the default transport.

Set these non-secret values alongside `APP_HOSTNAME`:

```
APP_HOSTNAME=hephaestus.build

HEPHAESTUS_WORKSPACE_SUBDOMAINS_ENABLED=false

HEPHAESTUS_WORKSPACE_SUBDOMAINS_BASE_DOMAIN=hephaestus.build

ACME_CHALLENGE=dnschallenge.provider

ACME_CHALLENGE_VALUE=cloudflare

TRAEFIK_DNS_CREDENTIALS_DIRECTORY=/etc/hephaestus/dns

TRAEFIK_DNS_CREDENTIAL_VARIABLE=CF_DNS_API_TOKEN_FILE
```

In pull-based production deployment, set the switch and base domain in both `app.env` and `proxy.env`. Set the ACME variables and credential-directory path in `proxy.env` only. The promotion workflow selects a verified release. It does not upload or replace host secrets. See [pull-based deployment](/admin/pull-based-deployment.md).

For the single-host installer, set these values in its one `.env` file.

With DNS-01 and a nonempty base domain, Traefik requests the apex and `*.<base>` certificate through its default TLS store, even while workspace subdomains stay off. Recreate the proxy and verify both certificate names before enabling workspace routing. The base domain must equal `APP_HOSTNAME` during preparation as well as after enablement. ACME state stays in the existing persistent volume.

Without these preparation settings, the default remains HTTP-01. Keep a working DNS-01 configuration during switch reversal if other services still use the wildcard certificate. See [Traefik ACME](https://doc.traefik.io/traefik/reference/install-configuration/tls/certificate-resolvers/acme/).

### Check and turn off

1. Upgrade the app and proxy to this release with the switch off.
2. Prepare the DNS-only wildcard record. Apply the proxy settings above with the switch off and verify the apex/wildcard certificate.
3. Register the exact apex OAuth callbacks and verify apex sign-in.
4. Set the switch and base domain in both app and proxy settings.
5. Recreate the app and proxy with Compose's `--wait` option.
6. Verify workspace navigation, public activity, sign-in, and an authenticated change on a workspace host.

The webapp container receives the same switch and base domain as the server. `APPLICATION_CLIENT_URL` is the apex app origin. When the switch is on, the web app container refuses to start if this value is missing. `APPLICATION_SERVER_URL` is the absolute apex API URL, including `/api`.

The reference Compose configuration supplies these values. For another deployment, supply all four values to the webapp container. Keep the server, webapp, and proxy settings consistent.

Recreate the app and proxy with Compose's `--wait` option. Invalid edge settings stop the proxy with a clear startup error instead of serving a partial configuration. Check the container logs if Compose reports a startup failure.

Check that a valid workspace host has a trusted certificate and serves the web app. Check that an apex `/w/<slug>` link redirects to the same path and query on its workspace host. Apex sign-in, consent, auth, legal, unsubscribe, instance administration, and settings stay on the apex. Tenant hosts must not serve API or OAuth paths. Do not add tenant hosts or broad wildcard expressions to `APP_HOST_MATCH`. That variable is only for the apex and explicit instance aliases.

To turn it off, set the switch to `false` in app and proxy settings and recreate both with `--wait`. Apex workspace links then serve the web app without a tenant redirect. No DNS record, credential, or ACME-state deletion is needed.

### Check the same-site precondition

Use a DNS base domain, not an IP address.

The base domain must not be a [public suffix](https://publicsuffix.org/list/). Check both the ICANN and PRIVATE sections of the current list. Include wildcard and exception entries in the check. Check that tenant hosts have the same registrable domain as the apex. A public suffix would put the apex and tenant hosts on different sites. The unchanged `SameSite` cookies would then prevent the tenant SPA from using the apex session.

See [MDN's site definition](https://developer.mozilla.org/en-US/docs/Glossary/Site).

## Register exact OAuth callbacks

Every login provider uses one callback on the configured auth issuer's origin. The server adds `api-base-path` and `/login/oauth2/code/<registrationId>`. Neither `Host` nor `X-Forwarded-Host` can change that callback. Instance administration shows the same callback that authentication uses.

1. Open **Login providers** in instance administration.
2. Copy each provider's callback URI.
3. If the registered address differs, register the exact URI shown.
4. In the GitHub login app settings, set **Callback URL** to `https://hephaestus.build/api/login/oauth2/code/github`.
5. Turn off **Enable wildcard matching in the callback URL** and remove tenant-host callbacks.
6. In the GitLab OAuth application, set **Redirect URI** to `https://hephaestus.build/api/login/oauth2/code/gitlab`.
7. In the Outline OAuth application, set **Redirect URI** to `https://hephaestus.build/api/login/oauth2/code/outline`.

Self-hosters replace `hephaestus.build` with their apex and use the registration ID shown in instance administration. Do not add a final slash or wildcard. Dynamic providers can have a different registration ID.

GitHub, GitLab, Slack sign-in, and Outline use this policy. The separate Slack workspace connection uses its configured `hephaestus.integration.slack.redirect-uri`. Set that URI to the exact apex `/api/oauth/callback/slack` address. GitHub App installations and GitLab token connections do not use a request-derived login callback.

If the registered callback already matches the configured issuer origin and API prefix, no callback update is needed. This includes existing callbacks registered at the apex. See [GitLab OAuth applications](https://docs.gitlab.com/integration/oauth_provider/) and [Outline's OAuth provider](https://docs.getoutline.com/s/hosting/doc/oauth-provider-9YJWoe3BJN). See [GitHub's callback guidance](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/about-the-user-authorization-callback-url). Exact callback comparison follows [OAuth Security Best Current Practice](https://www.rfc-editor.org/rfc/rfc9700.html#section-4.1.3).

## Browser access and CSRF

The CORS policy permits exact configured origins and installed extension origins as before. With subdomains on, it also permits an exact HTTPS origin with one valid, non-reserved workspace label. It checks the entire origin, not a suffix substring. The check does not look up a workspace. An unknown valid label gets the same CORS response as a current workspace label. The `docs` host is never a tenant origin, even if it appears in the configured origins.

Serve only the trusted Hephaestus SPA on tenant hosts. Never host uploaded HTML, third-party applications, or workspace-controlled scripts there. Reserve each label that another service uses before you enable subdomains. A trusted origin can read the browser session through the apex API.

The policy does not grant workspace membership. Every API request still passes the existing authorization checks.

The tenant SPA cannot read the apex `__Host-XSRF-TOKEN` cookie. It must fetch `GET https://<apex>/api/auth/csrf` with credentials. The response contains the raw `token` and its `headerName`. Send that header on unsafe requests to the apex, with credentials.

At startup, fetch the token before any other credentialed request. Sign-in and sign-out reload the page. The new page fetches the current token before it sends credentialed requests. Wait for the response before starting other requests.

A CSRF 403 has problem type `urn:hephaestus:csrf`. On that response, refetch the token once and retry the rejected request once. Do not retry another 403 or an ambiguous network failure. OAuth sign-in and browser sign-out clear the previous CSRF cookie through Spring Security.

The endpoint is available before sign-in and sends `Cache-Control: no-store`. Do not cache or compress this response at the edge. It must not appear in logs.

The auth cookies keep the `__Host-` prefix, `Secure`, `Path=/`, and no `Domain`. Return paths remain relative-only. Workspace detail and list responses expose the canonical `workspaceAddress` for the SPA.

## Browser state and navigation

Each origin keeps its own **View as user** session. The theme is the exception: the `hephaestus-theme` cookie on the base domain holds it, so it stays the same on every workspace host. The cookie can hold only `light`, `dark` or `system`, so another host under the base domain can change only the theme. No browser storage or session token is copied between hosts. The workspace-specific **View as user** entry opens on the workspace host before it stores the view. Instance administration stays on the apex.

Workspace administration uses `/admin` on the workspace host. This is the only shared prefix: internal `/admin` links target instance administration on the apex. Unambiguous apex paths, including `/workspaces/new`, leave the workspace host. Links to instance administration use absolute apex URLs.

Sign-in uses the apex `/login` page. The return path is relative, such as `/w/<slug>/activity?range=1y`. After sign-in, a document request reaches the edge's workspace redirect. The server does not accept an absolute return path. Slack connection callbacks derive their return workspace from the signed workspace ID, not browser storage.

The extension reads `/.well-known/hephaestus.json` on a pasted workspace host. This public, non-cacheable document names the configured apex and base domain. The extension asks for access to the apex before it connects. It does not guess a parent domain or send credentials during discovery.

## Workspace names and upgrade

A slug is a lowercase ASCII DNS label with 3 to 51 characters. It cannot start or end with a hyphen. Consecutive hyphens are prohibited. This also excludes `xn--` names. This follows [ADR 0053](https://github.com/hephaestus-build/Hephaestus/blob/main/docs/decisions/0053-workspace-addresses-are-a-presentation-origin.md).

Infrastructure and application names are reserved. These include `www`, `api`, `docs`, `admin`, `auth`, `login`, `mail`, `status`, `staging`, `preview`, and `pr<digits>`. The policy also reserves [RFC 2142 mailbox names](https://www.rfc-editor.org/rfc/rfc2142.html) and the application's route names.

The migration preserves valid slugs. It replaces an invalid or reserved slug with `workspace-<id>`. If that address was already used, it adds a numeric suffix. The migration records the old address in slug history. Old paths that match the previous slug format redirect directly to the current slug, with authorization unchanged.

Rename redirects do not expire or discard old entries. A used slug cannot name another workspace, even after workspace deletion. Database reservations enforce this across concurrent writes.

Make and verify a database backup before the upgrade. Check workspace addresses after the upgrade. The migration waits at most one second for each required table lock. If a long transaction prevents it from acquiring a lock, startup fails without a partial slug migration. Retry startup after that transaction finishes.

The database cannot recover history that an earlier version deleted. Reservations cover all current slugs and all retained history at upgrade time. Restore the verified pre-upgrade backup to reverse the migration.
