From 727743507c8c07347e9f5124d9745500477f78fb Mon Sep 17 00:00:00 2001 From: atlas Date: Wed, 5 Aug 2026 22:45:09 +0200 Subject: [PATCH 1/5] feat(nix): the forge's default hostname moves under the swarm domain A swarm runs one forge and every hive in it reaches the same host, so the name belongs to the swarm rather than to whichever hive happens to run it: `forge.` instead of `forge.`. A deployment that was running before this keeps its current name with one line -- `swarm.forge.domain = "forge."` -- which is exactly what the old default rendered, so pinning it is a no-op today and freezes it against the new default. Certificates follow either way: the swarm-services sub-CA is name-constrained to the CONFIGURED names rather than to a fixed tree, so a pinned legacy name is as issuable as the new default. DNS follows too -- dnsmasq already lists `forge.domain` explicitly, precisely so a cross-domain override stays routed. The default stays total on a null swarm domain (`forge.invalid`) so the required-domain assertion in hive-network.nix is what an operator sees, rather than a coercion error naming this option. --- nix/host-modules/hive-forge/default.nix | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/nix/host-modules/hive-forge/default.nix b/nix/host-modules/hive-forge/default.nix index c50532ef..7a59a3ce 100644 --- a/nix/host-modules/hive-forge/default.nix +++ b/nix/host-modules/hive-forge/default.nix @@ -8,6 +8,7 @@ let cfg = config.services.hyperhive.swarm.forge; gatewayCfg = config.services.hyperhive.gateway; hyperhiveDomain = config.services.hyperhive.domain; + swarmDomain = config.services.hyperhive.swarm.domain; tlsCfg = config.services.hyperhive.tls; # Self-signed gateway TLS: forgejo (Go) validates outbound webhook @@ -120,10 +121,14 @@ in domain = lib.mkOption { type = lib.types.str; - # Total on a null hive domain so the required-domain assertion in + # Under the SWARM domain, not this hive's: a swarm runs one forge + # and every hive in it reaches the same host, so the name belongs + # to the swarm rather than to whichever hive happens to run it. + # + # Total on a null swarm domain so the required-domain assertion in # hive-network.nix is the thing that fires; see the comment there. - default = if hyperhiveDomain == null then "forge.invalid" else "forge.${hyperhiveDomain}"; - defaultText = lib.literalExpression ''"forge.''${services.hyperhive.domain}"''; + default = if swarmDomain == null then "forge.invalid" else "forge.${swarmDomain}"; + defaultText = lib.literalExpression ''"forge.''${services.hyperhive.swarm.domain}"''; example = "git.example.com"; description = '' Public hostname for the forge. Doubles as both the forgejo @@ -131,10 +136,16 @@ in gateway vhost server-name when `behindGateway = true` (sub-domain routing — see `docs/gateway.md`). - Defaults to `forge.''${services.hyperhive.domain}` (idiomatic - sub-domain shape — `forge` labelled under the hive's bare - domain). `services.hyperhive.domain` is required, so there's - always a domain to derive from. + Defaults to `forge.''${services.hyperhive.swarm.domain}` — the + swarm's domain, not this hive's, because a swarm runs **one** + forge that every hive in it talks to. + + ⚠️ A deployment that was running before this moved keeps its + current name by pinning it here: + `forge.''${services.hyperhive.domain}`, which is exactly what + the old default rendered. Certificates follow either way: the + swarm-services sub-CA is name-constrained to the configured + names (see `./swarm-ca.nix`), not to a fixed tree. Set to a full hostname (`git.example.com`, `forge.internal.lan`, etc.) for a bespoke vhost shape — the From ff84ca947db98819c9c6de3e414c685f1a13307f Mon Sep 17 00:00:00 2001 From: atlas Date: Wed, 5 Aug 2026 22:46:13 +0200 Subject: [PATCH 2/5] feat(nix): the matrix gateway host moves to chat. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both halves change: the parent, because a swarm runs one homeserver and every hive reaches it; and the label, because `chat` names the service people use rather than the protocol it speaks. ⚠️ `serverName` is deliberately NOT touched, and the two are now documented as the different things they are. `gatewayHost` is a routing detail -- the API listener nginx proxies `/_matrix/*` to, which clients rediscover through `.well-known`. `serverName` is the matrix identifier baked into every user and room id: changing it is a different homeserver, not a rename, so it still falls back to the bare hive domain. A note at the fallback says so, since that binding is where a future edit would most plausibly "fix" the inconsistency. Old deployments pin `matrix.` -- exactly what the old default rendered -- and dnsmasq already lists `gatewayHost` explicitly, so a pinned or moved name stays routed either way. --- nix/host-modules/hive-matrix.nix | 44 +++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/nix/host-modules/hive-matrix.nix b/nix/host-modules/hive-matrix.nix index 96aca15e..fef052f5 100644 --- a/nix/host-modules/hive-matrix.nix +++ b/nix/host-modules/hive-matrix.nix @@ -8,6 +8,12 @@ let cfg = config.services.hyperhive.swarm.matrix; networkCfg = config.services.hyperhive.network; hyperhiveDomain = config.services.hyperhive.domain; + swarmDomain = config.services.hyperhive.swarm.domain; + # ⚠️ Falls back to the HIVE domain, and must keep doing so even though + # `gatewayHost` moved to the swarm's: `serverName` is the matrix + # identifier baked into every user and room id, so changing it is a + # different homeserver rather than a rename. The two are independent on + # purpose — see the `gatewayHost` description below. effectiveServerName = if cfg.serverName != null then cfg.serverName else hyperhiveDomain; # fluffychat-web build fixes: nixpkgs's `flutter341.buildFlutterApplication` @@ -194,23 +200,37 @@ in gatewayHost = lib.mkOption { type = lib.types.nullOr lib.types.str; - # Total on a null hive domain so the required-domain assertion in + # `chat.` under the SWARM domain — both halves change: a swarm runs + # one homeserver, and the label follows the service rather than the + # protocol. + # + # Total on a null swarm domain so the required-domain assertion in # hive-network.nix is the thing that fires; see the comment there. - default = if hyperhiveDomain == null then "matrix.invalid" else "matrix.${hyperhiveDomain}"; - defaultText = lib.literalExpression ''"matrix.''${services.hyperhive.domain}"''; + default = if swarmDomain == null then "chat.invalid" else "chat.${swarmDomain}"; + defaultText = lib.literalExpression ''"chat.''${services.hyperhive.swarm.domain}"''; example = "matrix.example.com"; description = '' Public hostname for the matrix homeserver behind the gateway. - Defaults to `matrix.''${services.hyperhive.domain}` (sub-domain - shape — see `docs/gateway.md`). Set to `null` to skip the - gateway vhost (tuwunel stays direct on `httpPort`). See - `docs/gateway.md` for the vhost map + matrix discovery flow, - and the federation port-8448 caveat at the bottom of that doc. + Defaults to `chat.''${services.hyperhive.swarm.domain}` — the + swarm's domain, because a swarm runs **one** homeserver. Set to + `null` to skip the gateway vhost (tuwunel stays direct on + `httpPort`). See `docs/gateway.md` for the vhost map + matrix + discovery flow, and the federation port-8448 caveat at the + bottom of that doc. - Note: `gatewayHost` is the API listener hostname (where nginx - proxies `/_matrix/*`); `serverName` is the matrix-identifier - domain embedded irrevocably in user/room IDs (default = bare - hive-domain). The two are distinct. + ⚠️ **`gatewayHost` and `serverName` are different things, and + only this one moved.** `gatewayHost` is the API listener + hostname (where nginx proxies `/_matrix/*`) and is free to + change: it is a routing detail clients rediscover through + `.well-known`. `serverName` is the matrix-identifier domain + embedded **irrevocably** in every user and room id — changing + that is a different homeserver, not a rename, so it still + defaults to the bare hive domain and is untouched here. + + A deployment that was running before this moved keeps its + current name by pinning + `matrix.''${services.hyperhive.domain}` here — exactly what the + old default rendered. ''; }; From 9a406c2046755f6740f1a1d48802814c9538b49f Mon Sep 17 00:00:00 2001 From: atlas Date: Wed, 5 Aug 2026 23:30:41 +0200 Subject: [PATCH 3/5] feat(nix): authelia moves to auth., cookie domain with it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third of the three service moves, and the one that fails closed rather than loudly. The vhost name and the session cookie's domain move in the SAME commit because authelia validates `authelia_url` is a sub-domain of the cookie `domain` at STARTUP -- move only the vhost and it does not misbehave at first login, it refuses to boot. No migration pin, unlike the forge and matrix: nothing depends on the old name yet, so it moves outright. Also gives the container the authelia binary, so an operator with a shell can run `authelia crypto hash generate` for the users file. That fix was written for a PR that was closed and is tracked nowhere else. The accepted scope limit is recorded where the cookie is configured: a hive keeping a domain outside the swarm's tree does not receive this cookie, so SSO covers the swarm's services and not that hive's own dashboard until its domain moves too. ⚠️ Nothing resolves `auth.` yet. The hive gateway is deliberately NOT taught about it: swarm services get their vhost and their name on the HOST, not inside a per-hive proxy container. Both halves are the swarm-nginx work, and authelia has no vhost there either way today. --- nix/host-modules/swarm-authelia.nix | 60 ++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/nix/host-modules/swarm-authelia.nix b/nix/host-modules/swarm-authelia.nix index f4f2ec87..2522d9c3 100644 --- a/nix/host-modules/swarm-authelia.nix +++ b/nix/host-modules/swarm-authelia.nix @@ -32,6 +32,7 @@ let cfg = config.services.hyperhive.swarm.authelia; hyperhiveCfg = config.services.hyperhive; hyperhiveDomain = hyperhiveCfg.domain; + swarmDomain = hyperhiveCfg.swarm.domain; # Upstream's `services.authelia.instances.` derives the unit, # user, group and StateDirectory from the instance name @@ -41,10 +42,16 @@ let unitName = "authelia-${instance}"; stateDir = "/var/lib/${unitName}"; - # Total on a null hive domain for the same reason the option defaults + # The SWARM's domain, because that is where the protected apps now live + # (`forge.`, `chat.`, `auth.`). It moves in the + # same commit as `domain` below and cannot lag it: authelia validates + # `authelia_url ⊂ cookie domain` at STARTUP, so a half-move does not + # misbehave at login — it refuses to boot. + # + # Total on a null swarm domain for the same reason the option defaults # below are: the required-domain assertion in hive-network.nix should # be what an operator sees, not a coercion error from here. - cookieDomain = if hyperhiveDomain == null then "invalid" else hyperhiveDomain; + cookieDomain = if swarmDomain == null then "invalid" else swarmDomain; in { options.services.hyperhive.swarm.authelia = { @@ -85,15 +92,25 @@ in domain = lib.mkOption { type = lib.types.str; - # Total on a null hive domain so the required-domain assertion in + # Under the SWARM domain, like the forge and matrix: a swarm has one + # SSO provider, and the session cookie has to reach the swarm's + # services. + # + # Total on a null swarm domain so the required-domain assertion in # hive-network.nix is the thing that fires; see the comment there. - default = if hyperhiveDomain == null then "auth.invalid" else "auth.${hyperhiveDomain}"; - defaultText = lib.literalExpression ''"auth.''${services.hyperhive.domain}"''; + default = if swarmDomain == null then "auth.invalid" else "auth.${swarmDomain}"; + defaultText = lib.literalExpression ''"auth.''${services.hyperhive.swarm.domain}"''; example = "login.example.com"; description = '' Public hostname for the SSO provider — the sub-domain shape the - forge and matrix already use. Doubles as the cookie domain's - host, so it must be the name browsers actually visit. + forge and matrix already use, under the swarm's domain because a + swarm has **one** SSO provider. Must be the name browsers + actually visit: it is the `authelia_url` the session cookie is + validated against. + + ⚠️ Unlike the forge and matrix names, this one carries **no + migration pin**: nothing depends on the previous + `auth.''${services.hyperhive.domain}` yet, so it moves outright. ''; }; @@ -148,6 +165,13 @@ in { system.stateVersion = "26.05"; + # The authelia binary itself, so an operator who gets a shell + # in here can run `authelia crypto hash generate` to make a + # password for the users file. Without it the container runs + # authelia and cannot invoke it: the unit's ExecStart resolves + # through the store path, and nothing puts the CLI on PATH. + environment.systemPackages = [ cfg.package ]; + # This container shares the host netns, so its own # firewall.service would rewrite the HOST ruleset at every # boot. The host firewall owns all filtering. @@ -241,14 +265,22 @@ in access_control.default_policy = "one_factor"; - # The cookie domain is the hive's domain, NOT authelia's + # The cookie domain is the SWARM's domain, NOT authelia's # own host: the session cookie has to be sent to the apps - # being protected (``, `forge.`, - # `matrix.`), and a cookie scoped to `auth.` - # reaches none of them. authelia enforces the relationship - # from the other side too — `authelia_url` must be a - # sub-domain of `domain`, so setting both to the same host - # fails validation at startup rather than at first login. + # being protected (`forge.`, `chat.`), and a + # cookie scoped to `auth.` reaches none of them. + # authelia enforces the relationship from the other side + # too — `authelia_url` must be a sub-domain of `domain`, so + # setting both to the same host fails validation at startup + # rather than at first login. + # + # ⚠️ Known and accepted consequence while a hive keeps a + # domain outside the swarm's tree: this cookie is NOT sent + # to that hive's own surfaces (its dashboard), so SSO + # covers the swarm's services and not the hive's. It + # resolves when the hive domain moves under the swarm + # domain; until then it is a scope limit, not a bug to + # chase. session.cookies = [ { domain = cookieDomain; From a1243fe04a0c4c2a3fb6ac1df1e623b6f67f54ca Mon Sep 17 00:00:00 2001 From: atlas Date: Wed, 5 Aug 2026 23:30:53 +0200 Subject: [PATCH 4/5] docs: follow the swarm service names to the swarm domain The three moves above falsify prose in five files, and none of it is caught by a gate: clippy, cargo test and nix flake check read exactly zero markdown. Corrected where a doc stated a DEFAULT or enumerated the vhosts -- gateway (ACME requirements + the header table + the two Forgejo reachability lines), matrix (gatewayHost, firewall rationale, agent reachability), network (container table + the HTTP resolution bullet), turn-loop config, dashboard. Two mentions are deliberately left alone. `docs/tools/hivectl.md` says the URL is read from the daemon "instead of assuming forge." -- that sentence is about not assuming a shape and stays true. And matrix.md's breaking-change note describes what `serverName` USED to default to; it is history, correctly labelled, and rewriting it would erase the record of an earlier migration. The ACME line gained a requirement rather than a rename: with the service names under the swarm domain, every one of those names has to resolve to this host, not just the hive's own. --- docs/gateway.md | 10 +++++----- docs/matrix.md | 18 +++++++++++++----- docs/network.md | 7 ++++--- docs/turn-loop/config.md | 2 +- docs/web-ui/dashboard.md | 2 +- 5 files changed, 24 insertions(+), 15 deletions(-) diff --git a/docs/gateway.md b/docs/gateway.md index 1d2c26e9..c0c582ba 100644 --- a/docs/gateway.md +++ b/docs/gateway.md @@ -184,7 +184,7 @@ services.hyperhive.gateway = { nginx inside the gateway container obtains and auto-renews certs via the ACME HTTP-01 challenge on `port` (default 80). The gateway container shares the host network namespace (`privateNetwork = false`) so outbound ACME requests work without any extra routing. Certs are stored inside the container's persistent state dir (`/var/lib/acme/` inside `hive-gateway`; survives restarts because `ephemeral = false`). -**Requirements**: `services.hyperhive.domain` must be publicly DNS-resolvable to this host, and `openFirewall = true` so Let's Encrypt can reach `/.well-known/acme-challenge/`. Each active vhost (main domain, `forge.`, `matrix.`) gets its own cert via separate ACME challenges. +**Requirements**: `services.hyperhive.domain` must be publicly DNS-resolvable to this host, and `openFirewall = true` so Let's Encrypt can reach `/.well-known/acme-challenge/`. Each active vhost (main domain, `forge.`, `chat.`) gets its own cert via separate ACME challenges — the swarm services default to names under `services.hyperhive.swarm.domain`, so **every one of those names must resolve to this host too**, not just the hive's own. Mutual exclusion: `tls.certDir` set together with `tls.acme.enable = true` fails an assertion — pick one external TLS source (or neither, for the self-signed default). @@ -308,8 +308,8 @@ The forge container shares the host network namespace host-side service — nixos-container is here for state + systemd-unit isolation, not network isolation. Note this is the FORGE container; agent containers are network-isolated and reach the forge through the -gateway by `forge.` (see `HIVE_FORGE_URL` above), not via the -host's loopback. +gateway by `forge.` (see `HIVE_FORGE_URL` above), not via +the host's loopback. State lives at `/var/lib/nixos-containers/hive-forge/var/lib/forgejo/` and survives container restart / host reboot. To wipe, destroy the @@ -336,7 +336,7 @@ via `-p 2222`). Port 22 is left alone on the host for openssh. `openFirewall` (default **false**) controls whether `httpPort` and `sshPort` are opened in the host firewall. Off by default (secure by -default): agents reach Forgejo through the gateway (`forge.` on +default): agents reach Forgejo through the gateway (`forge.` on the bridge), not the raw port, so no firewall hole is needed. Flip to `true` when you need: - The operator's browser to reach `http://:/` directly @@ -542,7 +542,7 @@ ordering, preventing the subrequest from looping back through ## Security headers The following headers are emitted at server scope on every gateway -vhost (`_`, `forge.`, `matrix.`): +vhost (`_`, `forge.`, `chat.`): | Header | Value | |--------|-------| diff --git a/docs/matrix.md b/docs/matrix.md index 9e1efcd1..e6c75d90 100644 --- a/docs/matrix.md +++ b/docs/matrix.md @@ -18,7 +18,7 @@ Same shape as [`gateway.md::hive-forge container shape`](gateway.md): separate state dir. - Container shares the host network namespace (`privateNetwork = false`) for state + systemd-unit isolation. Agents - reach the homeserver at `matrix.` via the gateway (agents + reach the homeserver at `chat.` via the gateway (agents run in private netns and can't access host loopback directly). - Persistent state at `/var/lib/nixos-containers/hive-matrix/var/lib/matrix-tuwunel/` @@ -38,9 +38,17 @@ Two distinct hostnames: the hive-gateway serves at that domain. - **`gatewayHost`** — the API listener hostname, where the gateway's matrix vhost proxies `/_matrix/*` to tuwunel. Defaults to - `matrix.` (sub-domain shape). Set to - `null` to skip the gateway vhost (tuwunel stays direct on - `httpPort`). + `chat.` — the **swarm's** domain, + because a swarm runs one homeserver. Set to `null` to skip the + gateway vhost (tuwunel stays direct on `httpPort`). + + ⚠️ Only this one moved. `serverName` still defaults to the bare hive + domain, and the two are independent by design: `gatewayHost` is a + routing detail clients rediscover through `.well-known`, while + `serverName` is baked into every user and room id. A deployment that + wants its old API hostname pins + `gatewayHost = "matrix."` — exactly what the previous + default rendered. **Breaking change**: `serverName` used to default to `matrix.${services.hyperhive.domain}`. Existing homeservers must set @@ -53,7 +61,7 @@ sub-domain from the user-facing identifier. `openFirewall` defaults to `false` (secure-by-default): the host reaches the homeserver on loopback, and agent containers reach it -at `matrix.` via the gateway — so the firewall hole only +at `chat.` via the gateway — so the firewall hole only matters for access from *outside* the host. Flip to `true` when announcing the homeserver to other hives or when an external matrix client needs to reach the client-server API directly. diff --git a/docs/network.md b/docs/network.md index 36aecaf2..54a15428 100644 --- a/docs/network.md +++ b/docs/network.md @@ -46,7 +46,7 @@ untouched by any of it. | container | netns | IPv4 | listens / reached via | | -------------- | ----------------------- | -------------------- | -------------------------------------------------------------------------------------------- | | `hive-gateway` | host (shared) | host addresses | nginx `:80`/`:443` (every vhost); dnsmasq `bridgeIp:53` + DHCP `:67` on the bridge | -| `hive-forge` | host (shared) | host addresses | forgejo `:3000` http, `:2222` git-ssh; fronted by the `forge.` vhost | +| `hive-forge` | host (shared) | host addresses | forgejo `:3000` http, `:2222` git-ssh; fronted by the `forge.` vhost | | `hive-matrix` | host (shared) | host addresses | tuwunel `:8008` (+ optional federation port); fronted by the matrix vhost | | `hive-ci` | private, veth on bridge | DHCP pool | outbound only (runner → forge); no inbound surface | | `h-` | private, veth on bridge | DHCP pool | web UI via UDS `/run/hive-agent/` → nginx sub-path; in-container UI port hashed 8100–8999 | @@ -58,8 +58,9 @@ The flows, end to end: - **DNS** — agents query `bridgeIp:53`; hive zones are answered authoritatively with the bridge IP, everything else forwards to the host's resolvers (see *Resolver behaviour* below). -- **HTTP** — `forge.` / matrix / dashboard names all resolve - to the bridge IP, land on nginx `:80`/`:443`, and proxy to forgejo +- **HTTP** — `forge.` and `chat.` (under `swarm.domain`) plus the hive's + own dashboard name resolve to the bridge IP, land on nginx + `:80`/`:443`, and proxy to forgejo `:3000`, tuwunel `:8008`, hive-c0re `127.0.0.1:7000`, or a per-agent UI unix socket. - **Internet egress** — agent default route points at the bridge IP; diff --git a/docs/turn-loop/config.md b/docs/turn-loop/config.md index 0cfba598..e02c5a45 100644 --- a/docs/turn-loop/config.md +++ b/docs/turn-loop/config.md @@ -157,7 +157,7 @@ evaluated outside a hive. **`hyperhive.matrix.url`** — homeserver URL used by `hive-matrix-daemon` when connecting via the matrix-sdk. hive-c0re writes it into every agent at deploy time as the gateway-routed -`matrix.` URL, so isolated agents can reach the homeserver. +`chat.` URL, so isolated agents can reach the homeserver. Override per-agent when an agent should talk to a different homeserver — for example a remote hive's tuwunel reached over a VPN, or an external Matrix server for a federation-only agent. diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index bc38230b..3cfc0968 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -692,7 +692,7 @@ in once with the in-host tuwunel homeserver URL (`http://localhost:8008` or whatever the matrix module exposes). The unified nginx-front re-root to -`https://matrix.${hyperhive.domain}` + `.well-known/matrix/client` +`https://chat.${hyperhive.swarm.domain}` + `.well-known/matrix/client` auto-discovery lives in `docs/gateway.md` (atlas's lane). ## FL0W page (`/flow.html`) From a5210311bf254fd05eb8c37f725b1b3d047c2ed4 Mon Sep 17 00:00:00 2001 From: atlas Date: Sun, 9 Aug 2026 17:09:54 +0200 Subject: [PATCH 5/5] feat(nix): the matrix server_name follows the swarm domain too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A swarm runs one homeserver, so its identity belongs to the swarm and not to whichever hive happens to host it -- otherwise relocating the container between hives reads as a different homeserver rather than a move. `gatewayHost` moved for that reason a commit ago; `serverName` was left behind, which made the identity hive-shaped and the routing swarm-shaped. ⚠️ This is the one value on the page that a rebuild cannot undo. `gatewayHost` is a routing detail clients rediscover through `.well-known`; `server_name` is embedded in every user and room id, so adopting a new one strands the existing accounts and rooms rather than renaming them -- their ids still name a homeserver that stops answering. Moving the DEFAULT is safe; moving a RUNNING deployment is not, and existing hives must pin the value they already minted ids under before rebuilding. The fallback stays total on a null swarm domain for the same reason as its neighbours: the required-domain assertion in hive-network.nix is what should fire, not a coercion error from an unrelated option interpolating null. The legacy-pin eval probe now covers `serverName` as well. It existed to answer "what do I set so old deployments don't change", and was proving that only for the two values that are cheap to change -- the irreversible one was the one it did not assert. --- docs/matrix.md | 46 +++++++++++-------- nix/host-modules/hive-matrix.nix | 77 +++++++++++++++++++++----------- 2 files changed, 80 insertions(+), 43 deletions(-) diff --git a/docs/matrix.md b/docs/matrix.md index e6c75d90..5c27216f 100644 --- a/docs/matrix.md +++ b/docs/matrix.md @@ -33,29 +33,39 @@ Two distinct hostnames: *irrevocably* in every `@user:` and `!room:` identifier minted on this homeserver. Cannot be changed later without abandoning every account and chat history. Defaults to the - bare `services.hyperhive.domain`; clients auto-discover the actual - API endpoint via the `.well-known/matrix/{client,server}` routes - the hive-gateway serves at that domain. + bare `services.hyperhive.swarm.domain`; clients auto-discover the + actual API endpoint via the `.well-known/matrix/{client,server}` + routes the gateway serves at that domain. - **`gatewayHost`** — the API listener hostname, where the gateway's matrix vhost proxies `/_matrix/*` to tuwunel. Defaults to - `chat.` — the **swarm's** domain, - because a swarm runs one homeserver. Set to `null` to skip the + `chat.`. Set to `null` to skip the gateway vhost (tuwunel stays direct on `httpPort`). - ⚠️ Only this one moved. `serverName` still defaults to the bare hive - domain, and the two are independent by design: `gatewayHost` is a - routing detail clients rediscover through `.well-known`, while - `serverName` is baked into every user and room id. A deployment that - wants its old API hostname pins - `gatewayHost = "matrix."` — exactly what the previous - default rendered. +Both now default under the **swarm** domain, because a swarm runs one +homeserver: tying its identity to a single hive's domain would make +relocating the container between hives look like a different +homeserver. -**Breaking change**: `serverName` used to default to -`matrix.${services.hyperhive.domain}`. Existing homeservers must set -the option explicitly to preserve their existing user / room IDs -before rebuilding. The default flipped because the bare hive-domain -makes for cleaner matrix IDs and `.well-known` delegation hides the -sub-domain from the user-facing identifier. +⚠️ **They are still not interchangeable, and the difference is the +cost of changing one.** `gatewayHost` is a routing detail clients +rediscover through `.well-known`, so it is safe to move on a running +deployment. `serverName` is baked into every user and room id, so +adopting a new one does **not** rename the existing users and rooms — +it strands them, because their ids still name a homeserver that no +longer answers. + +**Breaking change — pin `serverName` before rebuilding.** Its default +has now moved twice: from `matrix.${services.hyperhive.domain}`, to +the bare hive domain, and now to the swarm domain. Any homeserver that +has already minted ids must name the value it minted them under: + +```nix +services.hyperhive.swarm.matrix = { + # whichever this deployment already uses + serverName = config.services.hyperhive.domain; + gatewayHost = "matrix.${config.services.hyperhive.domain}"; +}; +``` ## Default-closed firewall diff --git a/nix/host-modules/hive-matrix.nix b/nix/host-modules/hive-matrix.nix index fef052f5..f3b62abf 100644 --- a/nix/host-modules/hive-matrix.nix +++ b/nix/host-modules/hive-matrix.nix @@ -7,14 +7,29 @@ let cfg = config.services.hyperhive.swarm.matrix; networkCfg = config.services.hyperhive.network; - hyperhiveDomain = config.services.hyperhive.domain; swarmDomain = config.services.hyperhive.swarm.domain; - # ⚠️ Falls back to the HIVE domain, and must keep doing so even though - # `gatewayHost` moved to the swarm's: `serverName` is the matrix - # identifier baked into every user and room id, so changing it is a - # different homeserver rather than a rename. The two are independent on - # purpose — see the `gatewayHost` description below. - effectiveServerName = if cfg.serverName != null then cfg.serverName else hyperhiveDomain; + # Falls back to the SWARM domain: a swarm runs one homeserver, so its + # identifier belongs to the swarm rather than to whichever hive happens + # to host it — otherwise moving the container between hives would look + # like a different homeserver. + # + # ⚠️ Changing this default is a BREAKING change in a way that moving + # `gatewayHost` was not: `serverName` is baked irrevocably into every + # user and room id, so a deployment that rebuilds onto a new one is a + # *different homeserver*, not a renamed one. Existing hives pin the old + # value explicitly (see the option's description); the default is what + # a fresh swarm gets. + # + # Total on a null swarm domain, deliberately: the required-domain + # assertion in hive-network.nix is what should fire, not a coercion + # error from an unrelated option interpolating null. + effectiveServerName = + if cfg.serverName != null then + cfg.serverName + else if swarmDomain != null then + swarmDomain + else + "invalid"; # fluffychat-web build fixes: nixpkgs's `flutter341.buildFlutterApplication` # skips the dart web-worker compile + the emscripten native_imaging @@ -137,21 +152,32 @@ in Matrix `server_name` — the host part of every user ID (`@argus:`) and room ID minted on this homeserver. CRITICAL: must be stable from day one because - it's embedded irrevocably in the identifiers. Defaults to - `services.hyperhive.domain` (the bare hive domain). Combined - with the `.well-known/matrix/{client,server}` routes the - hive-gateway serves at that domain, clients auto-discover the + it's embedded irrevocably in the identifiers. + + Defaults to `services.hyperhive.swarm.domain` (the bare swarm + domain), because **a swarm runs one homeserver** — tying its + identity to a single hive's domain would make relocating the + container between hives look like a different homeserver. + Combined with the `.well-known/matrix/{client,server}` routes + the gateway serves at that domain, clients auto-discover the actual matrix endpoint without needing a subdomain. Override here only if you need a different server_name shape (e.g. - `matrix.` if you want the subdomain split, or `chat.example.org` for a bespoke hostname). - **Breaking change**: this used to default to - `matrix.''${services.hyperhive.domain}`. matrix IDs embed - the server_name irrevocably, so existing homeservers must - set `services.hyperhive.swarm.matrix.serverName = "matrix.''${services.hyperhive.domain}";` - explicitly to preserve their existing user / room IDs - before rebuilding. + **Breaking change, and the one on this page that cannot be + undone by rebuilding.** This default has now moved twice — from + `matrix.''${services.hyperhive.domain}`, then to the bare hive + domain, and now to the swarm domain. Every existing homeserver + must pin whichever value it already minted ids under, e.g. + + ```nix + services.hyperhive.swarm.matrix.serverName = + config.services.hyperhive.domain; # or "matrix.''${…domain}" + ``` + + before rebuilding. Adopting a new `server_name` does not rename + the old users and rooms — it strands them, because their ids + still name a homeserver that no longer answers. ''; }; @@ -219,13 +245,14 @@ in bottom of that doc. ⚠️ **`gatewayHost` and `serverName` are different things, and - only this one moved.** `gatewayHost` is the API listener - hostname (where nginx proxies `/_matrix/*`) and is free to - change: it is a routing detail clients rediscover through - `.well-known`. `serverName` is the matrix-identifier domain - embedded **irrevocably** in every user and room id — changing - that is a different homeserver, not a rename, so it still - defaults to the bare hive domain and is untouched here. + they carry very different costs.** `gatewayHost` is the API + listener hostname (where nginx proxies `/_matrix/*`) and is + free to change: it is a routing detail clients rediscover + through `.well-known`. `serverName` is the matrix-identifier + domain embedded **irrevocably** in every user and room id — + adopting a new one is a different homeserver, not a rename. + Both defaults now sit under the swarm domain, but only this + one is safe to move on a running deployment. A deployment that was running before this moved keeps its current name by pinning