hyperhive/docs/gateway.md
atlas 922057e81b docs/gateway.md: extract big-picture gateway docs, trim in-code comments (mara #775:9988)
mara on PR #775: "this is too much docs in code - move bigger picture
stuff to md files and put refs in code"

New `docs/gateway.md` consolidates the gateway architecture story
that was spreading across long inline comments in `hive-gateway.nix`,
`hive-matrix.nix`, and `hive-forge.nix`:

- vhost map (which URL serves what, which upstream, which option)
- matrix discovery flow (.well-known → sub-domain delegation
  sequence)
- Accept-header SPA fallback pattern (#686 / #729 design history)
- local-dev `localHostsEntry` story
- sub-domain rationale (mara verdict tracking) + when sub-path is
  right (hyperhive-internal apps)
- per-vhost tuning knobs (forge LFS, matrix long-poll, agent SSE)
- sequencing history (which PR added which routing piece)

In-code comments in the two nix modules get trimmed to short refs
into the doc — keeps the *why* in the markdown while the *what*
stays alongside the code:

- hive-gateway.nix: top-of-file comment, `agentPortsTable`,
  `appendHttpConfig`, every location block + vhost
- hive-matrix.nix: `fluffychat-web-fixed`, `fluffychat-web-imaging`,
  the dart compile postInstall

README.md gets a new row in the docs table pointing at gateway.md.

Verified `nix eval` still resolves the same vhost + location layout
after the comment trim — no behavioral change, just less in-code
prose.
2026-05-31 14:29:40 +02:00

5.5 KiB

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
<hive>/ _ (catch-all) hive-c0re dashboard (7000) always
<hive>/agent/<name>/ _ per-agent harness on agent_web_port(name) agentPortsFile JSON, #15
<hive>/.well-known/matrix/{client,server} _ inline JSON (no upstream) matrix.enable && domain != null, #660 / #747
<hive>/matrix/ (deprecated) _ 301 → matrix.<hive>/ matrix.gui.enable, #772
forge.<hive>/ forge.<hive> forgejo (3000) forge.behindGateway, #754
matrix.<hive>/_matrix/* matrix.<hive> tuwunel (8008) matrix.gatewayHost != null, #764
matrix.<hive>/ matrix.<hive> fluffychat-web static matrix.gui.enable, #772
matrix.<hive>/config.json matrix.<hive> inline JSON (FluffyChat boot config) matrix.gui.enable && domain != null, #736

Per-agent UIs stay sub-path because they're hyperhive-internal and base-path-aware (iris #731). 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 <hive>. Sequence:

  1. Client fetches http://<hive>/.well-known/matrix/client{"m.homeserver":{"base_url":"http://matrix.<hive>"}} (no port suffix when gateway listens on 80).
  2. Client connects to matrix.<hive>/_matrix/client/....
  3. Gateway routes /_matrix/* → tuwunel at 127.0.0.1:8008.

Federation peers fetch .well-known/matrix/server{"m.server":"matrix.<hive>"} and connect to matrix.<hive>:8448 per spec default. Gateway only listens on configured port; cross-hive federation needs either an SRV record (_matrix._tcp.matrix.<hive> → port 80) 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 <hive> catch-all and the matrix.<hive> vhost both serve a flutter SPA (per-agent UI, fluffychat). Two requirements collide:

  • hard-refresh on a sub-route must serve index.html (SPA's client-side router takes over after JS bootstrap)
  • missing assets must surface as 404, not as HTML with wrong content-type (the original #643 bug)

Solution: an nginx http-context map $http_accept $matrix_spa_target { ... } keyed on the request's Accept header. Browser navigations (Accept: text/html,...) get index.html; asset fetches (Accept: image/*, */*, etc.) get a sentinel nonexistent path → try_files falls through to =404. No extension allowlist, no if block, no regex heuristics. #686 + #729 thread for the design history.

Local dev (localHostsEntry)

services.hyperhive.gateway.localHostsEntry = true adds entries to the host's /etc/hosts:

  • <hive-domain>127.0.0.1
  • forge.<hive>127.0.0.1 (when forge.behindGateway)
  • matrix.<hive>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)

mara verdict at #749:9609 + #747:9722: sub-domain over sub-path for forge + matrix, sub-path for per-agent UIs.

  • forgejo's default ROOT_URL = http://<host>/ works without any X-Forwarded-Prefix gymnastics — sub-domain hosting is the canonical Forgejo deploy shape.
  • matrix-spec deployments universally use matrix.<server_name> for the actual API listener — federation already expects this.
  • per-agent UIs are hyperhive-internal; iris's #731 made them base-path-aware specifically for /agent/<name>/. 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 — mara on #754:9684 wanted operator control over the full shape, not a forced <label>.<hive-domain> pattern.

Tuning knobs

Per-vhost timeouts + body-size limits live in the location blocks:

  • forge / (forgejo): client_max_body_size 1G (LFS), proxy_read_timeout 1h (multi-GB clones), proxyWebsockets = true (live-update endpoints).
  • matrix /_matrix/ (tuwunel): client_max_body_size 50M (media uploads), proxy_read_timeout 1h (long-poll /sync), CORS * (federation + cross-origin clients), proxyWebsockets = true.
  • per-agent /agent/<name>/: proxy_read_timeout 1d (long-lived SSE / WebSocket dashboards), proxyWebsockets = true, X-Forwarded-Prefix set so the harness can build absolute URLs when relative isn't enough.

SSH for forge stays direct on cfg.sshPort — separate listener protocol, not HTTP-over-nginx.

Sequencing history

  • #15 v0 (per-agent routing, #740) — first sub-app behind the gateway, JSON port table from c0re.
  • #686 / #729 — Accept-header SPA fallback pattern.
  • #749 / #754 — forge to sub-domain (mara: sub-domain over sub-path).
  • #747 / #764 — matrix sub-domain vhost + .well-known delegation.
  • #772 / #775 — fluffychat hops from <hive>/matrix/ to matrix.<hive>/.

Next-up tracked separately: #14 (container netns isolation), TLS (#594).