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.
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(noth-*) so c0re's lifecycle scanner ignores it; operator manages via the standardnixos-containerCLI. - 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 athttp://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-specserver_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 bareservices.hyperhive.domainper 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 tomatrix.<services.hyperhive.domain>(sub-domain shape per mara on #749:9609). Set tonullto skip the gateway vhost (tuwunel stays direct onhttpPort).
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.
- System activation writes a 32-byte random hex token (64
chars) to
cfg.registrationTokenFile(/var/lib/hyperhive/matrix-register-tokenby default), mode0600 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 readsregistration_token_file=""rejecting every registration until next restart. - Read-only bind-mount maps the host file into the tuwunel container at the same path.
- systemd
LoadCredential=inside the container copies the bind-mounted file into/run/credentials/tuwunel.service/registration_token, owned by tuwunel's dynamic user with mode0400, at service start. The host file staysroot:root 0600— nochown :tuwunel/chmod 0640/ GID-pin gymnastics required (per iris on #644 8043, dropping the shape #649 originally shipped with). KeepsDynamicUser = true+PrivateUsers = trueintact. - tuwunel's
registration_token_filepoints at the credentials path, not the original bind-mount path. - hive-c0re uses the token to register each agent account via
the matrix-spec UIAA registration flow, persists the returned
access_tokento<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 = trueat the protocol level so swarms can be wired up later by extendingtrustedServerswithout a homeserver restart.trusted_servers = []keeps it effectively closed until peers are listed.allow_registration = true(required for the token flow to engage). The absentyes_i_am_very_very_sure_…_open_registration_…flag keeps the server closed to anyone without the token.allow_encryption = falseper 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_nameis 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 asforge.domain(#754). Empty string renders.<hive>-shaped garbage in both nginxserver_name(treated as wildcard catch-all, surprising) and/etc/hosts(invalid entry).nullis 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 —buildFlutterApplicationonly runsflutter build webon 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-imagingbuildsImaging.{js,wasm}from thenative_imagingC source viapkgs.emscripten. Source comes frompkgs.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 = truebecause cmake runs insidejs/Makefileviaemcmake cmake, not at the package root. The build script needsHOME+EM_CACHEwritable for emscripten's on-demand sysroot build (libc, libc++ → wasm).fluffychat-web-fixedispkgs.fluffychat-webplus apostInstallpatch that (a) compilesweb/native_executor.dartviadart compile js(dart from the flutter341 closure, no incremental cost) and (b) installsfluffychat-web-imaging's outputs into$out.
Two non-obvious fixes from review history:
make -C jsinstead ofcd js; make(argus 🟡 on #697 v2) — keeps the build-phase pwd at the source root soinstallPhasedoesn't have to know about the cd. Robust against future reorders /dontBuild.web/native_executor.dartas a build-CWD-relative path, not$src/web/...(#685 / #733 fixup) —dart'spackage_config.jsonwalk-up needs to hitbuildFlutterApplication'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 —
openFirewalldefault flipped tofalse - #660 —
serverNamedefault flipped to bare hive-domain (wasmatrix.<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/tomatrix.<hive>/