# hive-gateway Single nginx in front of every hyperhive web surface. Container `hive-gateway`, shared host netns, system-config (not meta-flake managed). Configured via `services.hyperhive.gateway.*` + per-subsystem opt-in flags in `services.hyperhive.{forge,matrix,...}`. ## Vhost map | URL | vhost | upstream | source | | --- | --- | --- | --- | | `/` | `_` (catch-all) | dashboard dist (static, from `servedFrontend`) + API/SSE/actions → hive-c0re (`7000`) via `@c0re` | always | | `/agent//` | `_` | per-agent harness (UDS or TCP) | `agents.conf` (runtime-generated) | | `/.well-known/matrix/{client,server}` | `_` | inline JSON (no upstream) | `matrix.enable && domain != null` | | `/matrix/` (deprecated) | `_` | 301 → `matrix./` | `matrix.gui.enable` | | `forge./` | `forge.` | forgejo (`3000`) | `forge.behindGateway` | | `matrix./_matrix/*` | `matrix.` | tuwunel (`8008`) | `matrix.gatewayHost != null` | | `matrix./` | `matrix.` | fluffychat-web static | `matrix.gui.enable` | | `matrix./config.json` | `matrix.` | inline JSON (FluffyChat boot config) | `matrix.gui.enable && domain != null` | Per-agent UIs stay sub-path because they're hyperhive-internal and base-path-aware. External standard apps (forge / matrix) get sub-domains because their defaults work cleanly at sub-domain root + per-origin cookies / storage isolation matters. ## Discovery flow (matrix) Operator points client at ``. Sequence: 1. Client fetches `https:///.well-known/matrix/client` → `{"m.homeserver":{"base_url":"https://matrix."}}` (no port suffix when gateway listens on 443). The gateway always terminates TLS, so the scheme is always `https`; a non-default `httpsPort` is reflected as the port suffix. 2. Client connects to `matrix./_matrix/client/...`. 3. Gateway routes `/_matrix/*` → tuwunel at `127.0.0.1:8008`. matrix-dart-sdk (FluffyChat etc.) hardcodes `https` for the well-known fetch regardless of input scheme, so the discovery endpoint MUST be https — see "Self-signed TLS" below for the cert generation that backs the default-on path. Federation peers fetch `.well-known/matrix/server` → `{"m.server":"matrix."}` and connect to `matrix.:8448` per spec default. Gateway only listens on configured `port` (+ `httpsPort` when TLS on); cross-hive federation needs either an SRV record (`_matrix._tcp.matrix.` → port 80 / 443) OR `matrix.openFirewall = true` so peers reach tuwunel's federation port directly. Hyperhive is mostly closed/internal, so this rarely bites. ## SPA fallback (Accept-header pattern) The `` catch-all (operator dashboard), the per-agent UIs, and the `matrix.` vhost all serve a flutter/SPA bundle. Two requirements collide: - hard-refresh on a sub-route must serve `index.html` (SPA's client-side router takes over after JS bootstrap) - a non-navigation request that isn't an on-disk asset must NOT get HTML with the wrong content-type Solution: an `nginx http`-context `map $http_accept $_spa_target { ... }` keyed on the request's Accept header. Browser navigations (`Accept: text/html,...`) get `index.html`; everything else (`Accept: image/*`, `*/*`, `application/json`, `text/event-stream`, …) gets a sentinel nonexistent path, so `try_files $uri $_spa_target ` falls through to ``. No extension allowlist, no `if` block, no regex heuristics. The two vhosts differ only in ``: - **matrix / per-agent static assets** → `=404` (a missing asset is just missing). - **dashboard** → `@c0re` (a named location that reverse-proxies to hive-c0re `7000`). The dashboard's dynamic surface — every `/api/*`, the two SSE streams, the ~20 bare action/mutation routes (`/approve/{id}`, `/kill/{name}`, `/op-send`, …), and `/webhook/knowledge` — is all `Accept != text/html`, so it lands on `@c0re` automatically, **without enumerating a single backend prefix**. This is what lets the gateway static-serve the dashboard dist (from the `servedFrontend` nix-store path) while hive-c0re stays API-only — so a frontend-only change no longer rebuilds + restarts the core daemon. `@c0re` carries `proxy_buffering off` + a 1d read timeout (for the SSE streams) and a duplicated `auth_basic` block (named locations don't inherit it). Follow-up #1846 will move every backend route under `/api/`, collapsing this to a trivial `/api/* → c0re, else static` split. ## Local dev (`localHostsEntry`) `services.hyperhive.gateway.localHostsEntry = true` adds entries to the host's `/etc/hosts`: - `` → `127.0.0.1` - `forge.` → `127.0.0.1` (when forge.behindGateway) - `matrix.` → `127.0.0.1` (when matrix.gatewayHost set) `lib.unique` de-dupes if any sub-domain happens to equal another entry. Operators with real DNS leave it off. ## Sub-domain shape (rationale) Operator decision: sub-domain over sub-path for forge + matrix, sub-path for per-agent UIs. - forgejo's default `ROOT_URL = http:///` works without any `X-Forwarded-Prefix` gymnastics — sub-domain hosting is the canonical Forgejo deploy shape. - matrix-spec deployments universally use `matrix.` for the actual API listener — federation already expects this. - per-agent UIs are hyperhive-internal and base-path-aware specifically for `/agent//`. Sub-domain per agent would multiply DNS + TLS-per-subdomain cost without per-app config wins. - cookie / storage isolation: a future forge XSS can't reach the dashboard session because they're different origins. `services.hyperhive.{forge.domain,matrix.gatewayHost}` take the full hostname (`forge.darkest.space`, `git.example.com`) rather than a label that gets concatenated with hive-domain — operators want control over the full shape, not a forced `