hyperhive/docs/matrix.md
atlas a8d8159038 docs/matrix.md: extract hive-matrix prose (#718 batch 5)
New top-level matrix doc covering everything that doesn't fit
elsewhere:

- Container shape (nixos-container, shared host netns, name choice,
  state persistence) — sibling to gateway.md::hive-forge container shape.
- Identity vs API listener: serverName vs gatewayHost split with the
  #660 breaking change.
- Default-closed firewall + federation port 8448 caveat.
- Provisioning flow: registration token activation, bind-mount,
  LoadCredential, hive-c0re's per-agent register + access_token
  persistence. Captures #565 first-boot race + #644 / iris 8043
  ownership shape.
- Assertion rationale (serverName, gatewayHost == "").
- fluffychat-web build fixes (#685): Imaging.{js,wasm} emscripten
  derivation + dart compile worker fixup, build-CWD path lesson
  from #685 / #733.
- Sequencing history.

In-code # comments trim to short purpose statements + docs pointers.
description = '' blocks (operator-facing options docs) preserved per
iris #718. ~140 lines removed from hive-matrix.nix.

`nix flake check` clean; `nix fmt` clean.
2026-05-31 15:38:43 +02:00

9.1 KiB

hive-matrix

Private Matrix homeserver (matrix-tuwunel — the conduwuit successor) wrapped in a nixos-container, plus optional fluffychat-web client at matrix.<hive>/. Configured via services.hyperhive.matrix.*; vhost routing lives in gateway.md.

Container shape

Same shape as gateway.md::hive-forge container shape:

  • Container name hive-matrix (not h-*) so c0re's lifecycle scanner ignores it; operator manages via the standard nixos-container CLI.
  • Keeps hive-matrix from fighting any services.matrix-* the operator already runs on the host — separate systemd namespace, separate state dir.
  • Container shares the host network namespace (privateNetwork = false) so agents reach tuwunel at http://localhost:<httpPort> without extra plumbing — the nixos-container is here for state + systemd-unit isolation, not network isolation.
  • Persistent state at /var/lib/nixos-containers/hive-matrix/var/lib/matrix-tuwunel/ survives container restart / host reboot. To wipe, destroy the container.

Identity vs API listener: serverName vs gatewayHost

Two distinct hostnames:

  • serverName — matrix-spec server_name, embedded irrevocably in every @user:<server_name> and !room:<server_name> identifier minted on this homeserver. Cannot be changed later without abandoning every account and chat history. Defaults to the bare services.hyperhive.domain per mara on #660; clients auto-discover the actual API endpoint via the .well-known/matrix/{client,server} routes 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.<services.hyperhive.domain> (sub-domain shape per mara on #749:9609). Set to null to skip the gateway vhost (tuwunel stays direct on httpPort).

Breaking change (#660): serverName used to default to matrix.${services.hyperhive.domain}. Existing homeservers must set the option explicitly to preserve their pre-#660 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.

Default-closed firewall

openFirewall defaults to false (#651, secure-by-default): the homeserver is reachable from the host + every agent container via loopback either way (shared netns), 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.

Breaking change (#651): used to default to true. Operators relying on external reach must add services.hyperhive.matrix.openFirewall = true; before rebuilding.

Federation port 8448 is intentionally not opened here — tuwunel serves the federation API on the same httpPort as client-server by default. Reaching it on 8448 needs either an explicit tuwunel bind to that port OR a reverse-proxy + .well-known/matrix/server delegation (the latter lives in gateway.md::Discovery flow).

Provisioning flow (registration token)

Token-gated registration: hive-c0re holds the token, agents never see it. The agent only receives the resulting access_token.

  1. System activation writes a 32-byte random hex token (64 chars) to cfg.registrationTokenFile (/var/lib/hyperhive/matrix-register-token by default), mode 0600 root:root, before any container start. Idempotent — only writes when the file is missing or empty; always re-applies 0600 (normalises any 0640 / world-readable carry-over from pre-LoadCredential deployments). This runs at activation time (not first container start) to dodge the argus #565 race where nspawn creates an empty file when the bind-mount target is missing and tuwunel reads registration_token_file="" rejecting every registration until next restart.
  2. Read-only bind-mount maps the host file into the tuwunel container at the same path.
  3. systemd LoadCredential= inside the container copies the bind-mounted file into /run/credentials/tuwunel.service/registration_token, owned by tuwunel's dynamic user with mode 0400, at service start. The host file stays root:root 0600 — no chown :tuwunel / chmod 0640 / GID-pin gymnastics required (per iris on #644 8043, dropping the shape #649 originally shipped with). Keeps DynamicUser = true + PrivateUsers = true intact.
  4. tuwunel's registration_token_file points at the credentials path, not the original bind-mount path.
  5. hive-c0re uses the token to register each agent account via the matrix-spec UIAA registration flow, persists the returned access_token to <agent-state>/matrix-token. The agent's matrix MCP client authenticates with that access_token and never touches the shared registration token.

Initial rollout settings (#548):

  • allow_federation = true at the protocol level so swarms can be wired up later by extending trustedServers without a homeserver restart. trusted_servers = [] keeps it effectively closed until peers are listed.
  • allow_registration = true (required for the token flow to engage). The absent yes_i_am_very_very_sure_…_open_registration_… flag keeps the server closed to anyone without the token.
  • allow_encryption = false per operator call (#548). E2EE re-enabling tracked at #551.

Assertion rationale

Two config.assertions entries fail eval early rather than ship surprising behaviour:

  • hyperhiveDomain != null || cfg.serverName != null (mara on #548) — server_name is embedded into every user / room ID irrevocably; we refuse to spawn the homeserver with a bogus server_name we can never change later.
  • cfg.gatewayHost != "" (argus 🟡 on #764) — same footgun as forge.domain (#754). Empty string renders .<hive>-shaped garbage in both nginx server_name (treated as wildcard catch-all, surprising) and /etc/hosts (invalid entry). null is the right opt-out shape; empty string is rejected explicitly.

fluffychat-web build fixes (#685)

pkgs.fluffychat-web ships from flutter341.buildFlutterApplication, which has two upstream gaps for fluffychat's web target:

  • The dart web-worker entry point (web/native_executor.dart) isn't compiled — buildFlutterApplication only runs flutter build web on the main entry.
  • native_imaging's C source isn't built — emscripten isn't a flutter-builder native build input.

Both fixed in nix/modules/hive-matrix.nix via two derivations:

  • fluffychat-web-imaging builds Imaging.{js,wasm} from the native_imaging C source via pkgs.emscripten. Source comes from pkgs.fluffychat-web.passthru.pubspecLock.dependencySources.native_imaging — already in the build closure of the flutter app, so no parallel hash pin and version auto-syncs with nixpkgs bumps. Build closure is ~3.6 GiB (emscripten LLVM); runtime closure is just the two output files. dontConfigure = true because cmake runs inside js/Makefile via emcmake cmake, not at the package root. The build script needs HOME + EM_CACHE writable for emscripten's on-demand sysroot build (libc, libc++ → wasm).
  • fluffychat-web-fixed is pkgs.fluffychat-web plus a postInstall patch that (a) compiles web/native_executor.dart via dart compile js (dart from the flutter341 closure, no incremental cost) and (b) installs fluffychat-web-imaging's outputs into $out.

Two non-obvious fixes from review history:

  • make -C js instead of cd js; make (argus 🟡 on #697 v2) — keeps the build-phase pwd at the source root so installPhase doesn't have to know about the cd. Robust against future reorders / dontBuild.
  • web/native_executor.dart as a build-CWD-relative path, not $src/web/... (#685 / #733 fixup) — dart's package_config.json walk-up needs to hit buildFlutterApplication's pub-get output (.dart_tool/ in the build CWD). Walking up from a read-only $src/ store path finds no .dart_tool/ and errors with "Couldn't resolve the package 'matrix'". Confused two PRs.

Drop both derivations when nixpkgs's flutter builder grows worker

  • emcc support upstream.

Mount point is matrix.<hive>/ (#772); upstream --base-href "/" is correct at sub-domain root, no override.

Sequencing history

  • #548 — initial rollout (federation enabled, registration enabled, E2EE disabled)
  • #565 — first-boot empty-token race fix → activation-time token generation
  • #644 / iris 8043 / #649 — registration token ownership shape (dropped chown/GID-pin; LoadCredential delivers as 0400 dynamic-user)
  • #651 — openFirewall default flipped to false
  • #660 — serverName default flipped to bare hive-domain (was matrix.<hive>)
  • #685 / #697 / #733 — fluffychat-web build fixes (Imaging emscripten, native_executor dart worker, build-CWD path)
  • #736 — fluffychat config.json inline JSON at sub-domain root
  • #749 / #764 — gateway sub-domain shape verdict
  • #772 / #775 — fluffychat hops from <hive>/matrix/ to matrix.<hive>/