Compare commits

...
Author SHA1 Message Date
atlas
8d25b087b6 nix/hive-matrix: trim option-description prose to docs/gateway.md refs (argus #775 🔴)
argus on #775 v3: "the `gatewayHost` option description's
server_name-vs-gatewayHost essay + federation SRV note are also
candidates for [docs/gateway.md] section."

Cuts the gatewayHost option's description from ~40 lines (with
inline duplication of the discovery flow, when-to-set-which, and
federation port caveat) down to ~8 lines pointing at
`docs/gateway.md`. The brief `server_name vs gatewayHost` clarifier
stays in code because it disambiguates two SIMILAR-LOOKING options
on the same module — operators reading option docs need the
distinction inline, not behind a doc link.

Also trimmed `matrix.gui.enable` + `matrix.gui.package` descriptions
to similar shapes — point at docs/gateway.md for the architecture,
keep the override-shape hints in code.

Push includes the rebase onto current main (#764 + 0af6ea1 + others
landed since #775 was opened; cherry-picked commits get skipped
cleanly).

Net: matrix.nix loses ~70 lines of inline prose. No behavioral
change (verified gatewayHost still resolves to `matrix.<hive>`).
2026-05-31 14:29:40 +02:00
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
atlas
df549ed2a5 nix/hive-{matrix,gateway}: host fluffychat at matrix.<hive>/, 301 from <hive>/matrix/ (#772)
mara on #764:9897: "host the fluffy chat app at / as follow up?"

Moves fluffychat-web from the bare-domain sub-path
(`<hive>/matrix/`) to the matrix sub-domain root
(`matrix.<hive>/`). Follow-up to #764 (matrix vhost itself), per
mara's gateway-architecture verdict (sub-domain for external standard
apps, sub-path for hyperhive-internal). Stacked on
`atlas/747-matrix-behind-gateway` — depends on #764 landing first.

## Mechanics

**hive-matrix.nix:**
- Drop `flutterBuildFlags = [ "--base-href" "/matrix/" ]` from
  `fluffychat-web-fixed`. Upstream default `--base-href "/"` is correct
  at sub-domain root.
- Update option docs to reflect new mount point.

**hive-gateway.nix:**
- `$matrix_spa_target` map target flips from `/matrix/index.html` →
  `/index.html` (sub-domain root now).
- New `<hive>/matrix/*` location: `rewrite ^/matrix/(.*)$
  matrix.<hive>/$1 permanent;` — 301 redirect preserves bookmark +
  deep-link compatibility for `<hive>/matrix/#/rooms/...` URLs during
  the transition.
- `<hive>/matrix/config.json` location removed (moved to `/config.json`
  on the matrix vhost).
- Matrix vhost (#764) gains `/` location: serves fluffychat dist as
  static files with the Accept-header SPA fallback (`/_matrix/`
  proxying to tuwunel keeps working via nginx longer-prefix-wins
  precedence). When `gui.enable = false`, `/` returns 404 cleanly.
- Matrix vhost gains `= /config.json` for the FluffyChat boot-config
  pre-fill (#736).

## Verified

```
vhosts:        ["_", "forge.test.local", "matrix.test.local"]
bare locations: ["/", "/matrix/", "= /.well-known/matrix/client",
                 "= /.well-known/matrix/server"]
matrix vhost locations: ["/", "/_matrix/", "= /config.json"]
/matrix/ extraConfig: "rewrite ^/matrix/(.*)$ http://matrix.test.local/$1 permanent;"
matrix vhost / alias: /nix/store/...fluffychat-web-2.6.0/
```

Full container toplevel builds clean.

## Risk

Medium. Two breaking changes for operators:

1. **Bookmark migration**: `http://<hive>/matrix/#/rooms/...` 301s
   to `http://matrix.<hive>/#/rooms/...`. Browser bookmarks +
   shared links keep working via the redirect; can be cleaned up
   once it's been in the wild long enough.

2. **fluffychat-web dist hash changes**: dropping the
   `--base-href "/matrix/"` flag changes the derivation hash, so
   `gui.package` rebuilds even though the source is the same.
   Operators on substitute caches will fetch the new dist; building
   from source takes the same time as before.

The `.well-known/matrix/{client,server}` delegation (already
advertising `matrix.<hive>` per #764) means matrix clients
auto-discover the new location — no client config change needed.

## Sequencing

**Depends on #764** — needs the matrix vhost to host the new `/`
location. Merge after #764 lands + soaks. If #764 changes shape
during review I'll rebase + force-push.

Closes #772.
2026-05-31 14:29:40 +02:00
4 changed files with 229 additions and 408 deletions

View file

@ -42,6 +42,7 @@ Depth lives in [`docs/`](docs/) — pick the one matching your task:
| config-edit + approval state machine | [`docs/approvals.md`](docs/approvals.md) |
| what survives destroy / purge / restart | [`docs/persistence.md`](docs/persistence.md) |
| naming, wire protocol, commit style | [`docs/conventions.md`](docs/conventions.md) |
| nginx vhost map + sub-domain routing | [`docs/gateway.md`](docs/gateway.md) |
| NixOS / nspawn gotchas | [`docs/gotchas.md`](docs/gotchas.md) |
## Host config

78
docs/gateway.md Normal file
View file

@ -0,0 +1,78 @@
# 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).

View file

@ -10,25 +10,10 @@ let
matrixCfg = config.services.hyperhive.matrix;
forgeCfg = config.services.hyperhive.forge;
# Per-agent port table for `/agent/<name>/` routing (#15 v0). Single-
# sourced from `cfg.agentPortsFile` (default
# `/var/lib/hyperhive/agent-ports.json`), written by hive-c0re on every
# topology change in shape `{ "<name>": <port>, ... }`.
#
# Read at deploy time via `builtins.fromJSON (builtins.readFile ...)`
# — pure eval (the file lives outside the nix store; nix copies the
# content into the store as a fixed-output dep). When the file is
# missing (fresh install before c0re has had a chance to write it),
# default to an empty map → no per-agent routes generated → gateway
# falls back to its pre-#15 shape. The container rebuilds on every
# `hivectl gateway-sync` (operator-initiated) or on the next
# `nixos-rebuild switch`, picking up whatever c0re has written
# since the last build.
#
# mara on #740 (comment 9295) + #15 (comment 9270): the gateway
# nginx container lives in system config (not meta), so it can't
# auto-rebuild from meta-flake events — the JSON file is what
# bridges the host's nix eval to the agent-lifecycle data c0re owns.
# Per-agent port table for `/agent/<name>/` routing. C0re writes
# this JSON on every topology change; gateway reads at deploy time.
# Missing file → empty map → no per-agent routes (graceful default).
# See `docs/gateway.md` for the discovery + rebuild flow.
agentPortsTable =
if cfg.agentPortsFile == null || !builtins.pathExists cfg.agentPortsFile then
{ }
@ -36,21 +21,11 @@ let
builtins.fromJSON (builtins.readFile cfg.agentPortsFile);
in
{
# Single nginx in front of every hyperhive surface (#609 / #15 v0).
# Lives in its own nixos-container (like hive-forge / hive-matrix) so
# the operator can opt out without touching the host's own nginx, and
# so the static-serve responsibility for the matrix GUI moves off
# hive-c0re's axum router. Shares host netns so `localhost`
# upstream resolution works without any port-forward dance.
#
# Routes (v0):
# `location /matrix/` → static-serve fluffychat-web dist (when
# `services.hyperhive.matrix.gui.enable` is true)
# `location /` → proxy_pass to hive-c0re's dashboard upstream
#
# Container name `hive-gateway` keeps hive-c0re's lifecycle scanner
# (which only sees `h-*`) out of the picture. State-free — nginx
# config lives in the nix store, no runtime persistence to manage.
# Single nginx in front of every hyperhive web surface — dashboard,
# per-agent UIs (sub-path), forge + matrix (sub-domain), .well-known
# delegations. Container `hive-gateway`, shared host netns,
# state-free. Full vhost map + discovery flow + design rationale in
# `docs/gateway.md`.
options.services.hyperhive.gateway = {
enable = lib.mkOption {
@ -209,33 +184,15 @@ in
enable = true;
recommendedProxySettings = true;
recommendedOptimisation = true;
# SPA-fallback target keyed on the `Accept` request header
# (#686, mara + damocles on PR #729). This decides whether a
# `/matrix/...` miss falls through to `index.html` (route
# navigation) or returns a clean 404 (asset miss) — see the
# `/matrix/` location comment below for the full rationale.
#
# Top-frame browser navigations always send
# `Accept: text/html,...` (chrome/firefox/safari are
# consistent on this). Asset fetches from script tags / img
# / fetch() / XHR send asset-typed Accepts (`image/*`,
# `application/javascript`, `*/*`) without `text/html`.
# Mapping is purely on the header → no extension allowlist
# to keep in sync with whatever the SPA ships, no regex
# heuristic to false-positive on dot-segment routes.
#
# `$matrix_spa_target` defaults to a sentinel nonexistent
# path so `try_files` falls through to the trailing `=404`
# for asset misses. Browser navigations route to
# `/matrix/index.html` where the SPA's client-side router
# takes over.
#
# Only emitted when the matrix GUI is on (saves a no-op
# `map` directive otherwise).
# Accept-header SPA fallback (#686 / #729): navigations
# (`Accept: text/html,...`) fall to index.html, asset
# fetches (Accept *anything else*) fall to a sentinel
# nonexistent path → `try_files` returns 404. Pattern
# detailed in `docs/gateway.md` ("SPA fallback").
appendHttpConfig = lib.optionalString (matrixCfg.enable && matrixCfg.gui.enable) ''
map $http_accept $matrix_spa_target {
default "/__matrix_spa_no_html_fallback";
"~*text/html" "/matrix/index.html";
"~*text/html" "/index.html";
}
'';
virtualHosts = {
@ -247,99 +204,37 @@ in
}
];
locations =
# Matrix GUI: when the operator has flipped both
# `services.hyperhive.matrix.enable` and `matrix.gui.enable`
# on, nginx serves fluffychat-web (or whatever override)
# as a static dist at `/matrix/`.
#
# SPA fallback (iris/#643, rewritten in #686 per mara
# + damocles on PR #729): the original
# `try_files $uri $uri/ /matrix/index.html;` shape
# silently masked missing assets — flutter's bootstrap
# requesting e.g. `/matrix/native_executor.js` got
# `index.html` (Content-Type: text/html, status 200)
# when the file was absent from the dist, so the JS
# runtime never loaded and `/matrix/` rendered blank
# without any visible error.
#
# The followup #729 narrowed it with an extension
# allowlist; this version uses `$matrix_spa_target`
# (defined in the `appendHttpConfig` above, keyed on
# the `Accept` header) so the decision lives in HTTP
# semantics rather than a maintained extension list.
# Navigations (Accept: text/html) fall to index.html;
# asset fetches (Accept: */*, image/*, etc.) get a
# clean 404 via the trailing `=404`.
lib.optionalAttrs (matrixCfg.enable && matrixCfg.gui.enable) {
"/matrix/" = {
alias = "${matrixCfg.gui.package}/";
extraConfig = ''
try_files $uri $uri/ $matrix_spa_target =404;
'';
};
# FluffyChat fetches `/matrix/config.json` directly on
# boot for its own branding + default-homeserver
# bootstrap, BEFORE asking the user to pick a server
# (#736). The upstream `pkgs.fluffychat-web` dist
# ships without one, so the fetch 404s and the user
# sees the empty "enter homeserver" prompt. Serve a
# minimal config that pre-fills `defaultHomeserver`
# with the operator's hive domain — the matrix
# client then runs `.well-known/matrix/client` against
# that domain (already served by the
# `= /.well-known/matrix/client` block below) and
# discovers the actual tuwunel endpoint.
#
# Only the `defaultHomeserver` field is overridden —
# everything else (branding, audio defaults, etc.)
# falls back to fluffychat's hardcoded defaults so
# we don't pin against upstream config-schema drift.
# No-op when `services.hyperhive.domain` is unset
# (the location block is omitted entirely; the SPA
# then falls back to its empty form, same as before
# #736).
} // lib.optionalAttrs (
matrixCfg.enable && matrixCfg.gui.enable && hyperhiveDomain != null
) {
"= /matrix/config.json" = {
extraConfig = ''
default_type application/json;
return 200 '{"defaultHomeserver":"${hyperhiveDomain}"}';
'';
};
}
# `<hive>/matrix/*` → 301 → `matrix.<hive>/$1`
# (fluffychat moved to sub-domain root in #772; this
# keeps bookmarks + deep-links working during the
# transition). See `docs/gateway.md` for the vhost
# map.
lib.optionalAttrs (
matrixCfg.enable
&& matrixCfg.gui.enable
&& matrixCfg.gatewayHost != null
) (
let
portSuffix = if cfg.port == 80 then "" else ":${toString cfg.port}";
target = "http://${matrixCfg.gatewayHost}${portSuffix}";
in
{
"/matrix/" = {
extraConfig = ''
rewrite ^/matrix/(.*)$ ${target}/$1 permanent;
'';
};
}
)
//
# `.well-known/matrix/*` auto-discovery (#660): when
# `services.hyperhive.matrix.enable` is on and the
# operator's set a hive domain, the gateway serves
# the matrix-spec discovery JSON at the canonical
# location so clients pointed at `${hyperhive.domain}`
# resolve through to the actual tuwunel endpoint
# without needing a `matrix.` subdomain.
#
# `m.homeserver.base_url` advertises the client-server
# API. `m.server` advertises the federation
# `host:port` (tuwunel serves both client + federation
# on the same `httpPort` — see hive-matrix.nix).
#
# CORS `*` on the client endpoint per the matrix spec
# (https://spec.matrix.org/v1.15/client-server-api/#getwell-knownmatrixclient).
# No-op until the operator turns matrix on; until then
# there's no homeserver to advertise.
# `.well-known/matrix/{client,server}` discovery JSON.
# Points clients at `matrixCfg.gatewayHost` (sub-domain
# vhost) when set; falls back to direct `<hive>:<httpPort>`
# when no gateway target. CORS `*` per matrix spec.
# See `docs/gateway.md` "Discovery flow" for the full
# client-bootstrap sequence.
lib.optionalAttrs (matrixCfg.enable && hyperhiveDomain != null) (
let
# `.well-known/matrix/{client,server}` advertise where
# the actual matrix API lives. When `matrixCfg.gatewayHost`
# is set (default `matrix.<hive-domain>`, #747), point
# at the sub-domain — no port suffix when the gateway
# is on the canonical port 80, transparent to clients
# (mara on #749:9609 sub-domain verdict, "not user-
# visible because the .well-known redirect routes
# clients through automatically"). When `gatewayHost`
# is unset (no hive-domain, or operator nulled it),
# fall back to the direct `host:port` shape — clients
# reach tuwunel without going through the gateway,
# no sub-domain delegation.
portSuffix = if cfg.port == 80 then "" else ":${toString cfg.port}";
clientBaseUrl =
if matrixCfg.gatewayHost != null then
@ -369,33 +264,12 @@ in
}
)
//
# Per-agent UIs (#15 v0). One `/agent/<name>/`
# block per `<name>: <port>` entry in
# `agentPortsTable` (loaded from `cfg.agentPortsFile`
# — `/var/lib/hyperhive/agent-ports.json` by default,
# written by hive-c0re on every topology change).
#
# Trailing-slash pair (`/agent/<name>/` + `proxy_pass
# http://...:<port>/`) strips the `/agent/<name>`
# prefix on the upstream side, so the agent server
# receives `GET /` for the SPA root, `GET /api/state`
# for the API, `GET /screen/ws` for the websocket, etc.
# The agent's emitted asset URLs are document-relative
# (iris's #731) so they round-trip back through the
# gateway under the same prefix without the harness
# needing prefix-awareness.
#
# `X-Forwarded-Prefix` set so the harness can build
# correct absolute URLs for any case where relative
# isn't enough (server-emitted redirects, OG meta
# tags, etc.).
#
# SSE / websocket support via `proxyWebsockets = true`
# (same as the c0re `/` block below).
#
# Empty / missing `cfg.agentPortsFile` → empty table
# → no per-agent blocks; old `<host>:<port>/` direct
# reach still works.
# Per-agent UIs (#15 v0). One `/agent/<name>/` block
# per entry in `agentPortsTable`. Trailing-slash pair
# strips the prefix; `X-Forwarded-Prefix` lets the
# harness build absolute URLs when relative isn't
# enough. See `docs/gateway.md` for the vhost map
# + tuning rationale.
lib.mapAttrs' (name: port: {
name = "/agent/${name}/";
value = {
@ -425,31 +299,11 @@ in
};
}
//
# Forge vhost (#749, mara verdict at issue:9609 —
# sub-domain over sub-path). When forgejo runs behind the
# gateway (`forge.behindGateway = true`), it gets its own
# `server { server_name = forge.domain; }` block. The
# block proxies all `/` → `http://127.0.0.1:<forge.httpPort>/`
# so forgejo handles requests at root (default deploy shape
# — no `ROOT_URL`-prefix translation needed).
#
# `forge.domain` is the full hostname (e.g.
# `forge.darkest.space`, `git.example.com`) — single source
# of truth for both the forgejo `DOMAIN` setting and the
# gateway vhost name (mara on #754:9684 — "specify full
# forge domain in options instead").
#
# `client_max_body_size 1G` — git pushes + LFS uploads can
# be large; nginx's default 1M would 413 most real commits.
#
# Long timeouts for big repo operations: a fresh clone of a
# multi-GB repo can take minutes; the default 60s
# `proxy_read_timeout` would abort mid-stream.
#
# `proxyWebsockets = true` keeps forgejo's live-update
# endpoints (`/api/v1/events`) + any future websocket
# endpoints working transparently. SSH stays direct on
# `forge.sshPort` (separate listener protocol, not HTTP).
# Forge sub-domain vhost (#749 / #754). `server_name =
# forge.domain`, proxies all `/` → forgejo. Tuned for
# git: `client_max_body_size 1G`, `proxy_read_timeout 1h`
# (multi-GB clones). SSH stays direct on `forge.sshPort`.
# See `docs/gateway.md`.
lib.optionalAttrs (forgeCfg.enable or false && forgeCfg.behindGateway or false) {
"${forgeCfg.domain}" = {
listen = [
@ -471,34 +325,12 @@ in
};
}
//
# Matrix homeserver vhost (#747, mara verdict on #749:9609 —
# sub-domain over sub-path for matrix; "not user-visible"
# because clients discover the sub-domain via the
# `.well-known/matrix/{client,server}` delegation served
# above on the bare hive-domain).
#
# `server { server_name = matrixCfg.gatewayHost; }` proxies
# `/_matrix/...` → `http://127.0.0.1:''${matrixCfg.httpPort}/_matrix/...`.
# Tuwunel listens on `:''${httpPort}` (default 8008); the
# gateway terminates on `:''${cfg.port}` (80) so external
# clients speak matrix over the canonical web port without
# operators having to open the tuwunel port through firewalls.
#
# `/` returns 404 — nothing else lives at the matrix vhost;
# the matrix client-server API is entirely under `/_matrix/`,
# and federation under `/_matrix/federation/...`.
#
# CORS `*` on the matrix vhost per the matrix spec —
# federation + client requests come from any origin.
#
# `client_max_body_size 50M` covers typical media uploads
# (matrix-spec media size cap default); operators with bigger
# uploads override via the matrix module's own cap when that
# lands.
#
# `proxy_read_timeout 1h` for long-poll `/sync`; the default
# 60s would abort `/sync?timeout=30000` legitimately when
# tuwunel's keepalive exceeds that.
# Matrix sub-domain vhost (#747 / #764). `server_name =
# matrixCfg.gatewayHost`. `/_matrix/*` → tuwunel (CORS *,
# 50M body cap, 1h long-poll timeout). `/` serves
# fluffychat (#772) or 404 if GUI off. nginx
# longer-prefix-wins puts `/_matrix/` ahead of `/`.
# See `docs/gateway.md`.
lib.optionalAttrs (matrixCfg.enable && matrixCfg.gatewayHost != null) {
"${matrixCfg.gatewayHost}" = {
listen = [
@ -507,22 +339,48 @@ in
port = cfg.port;
}
];
locations = {
"/_matrix/" = {
proxyPass = "http://127.0.0.1:${toString matrixCfg.httpPort}";
proxyWebsockets = true;
extraConfig = ''
proxy_buffering off;
client_max_body_size 50M;
proxy_read_timeout 1h;
proxy_send_timeout 1h;
add_header Access-Control-Allow-Origin *;
'';
locations =
{
"/_matrix/" = {
proxyPass = "http://127.0.0.1:${toString matrixCfg.httpPort}";
proxyWebsockets = true;
extraConfig = ''
proxy_buffering off;
client_max_body_size 50M;
proxy_read_timeout 1h;
proxy_send_timeout 1h;
add_header Access-Control-Allow-Origin *;
'';
};
}
// lib.optionalAttrs (matrixCfg.gui.enable) (
{
# fluffychat at sub-domain root, SPA-fallback via
# the Accept-header `$matrix_spa_target` map.
"/" = {
alias = "${matrixCfg.gui.package}/";
extraConfig = ''
try_files $uri $uri/ $matrix_spa_target =404;
'';
};
}
// lib.optionalAttrs (hyperhiveDomain != null) {
# FluffyChat boot-config pre-fill so the client's
# `.well-known/matrix/client` lookup hits the
# right delegation endpoint (#736).
"= /config.json" = {
extraConfig = ''
default_type application/json;
return 200 '{"defaultHomeserver":"${hyperhiveDomain}"}';
'';
};
}
)
// lib.optionalAttrs (!matrixCfg.gui.enable) {
"/" = {
return = "404";
};
};
"/" = {
return = "404";
};
};
};
};
};
@ -533,17 +391,10 @@ in
allowedTCPPorts = [ cfg.port ];
};
# `/etc/hosts` entries for local dev: the bare hive domain plus
# any sub-domain modules (forge via #749/#754, matrix via #747)
# that are on. All map to `127.0.0.1` since the gateway shares
# host netns. Operators with real DNS leave `localHostsEntry =
# false`; this is the dev-loop shortcut for `http://<hive-domain>/`
# + `http://forge.<hive-domain>/` + `http://matrix.<hive-domain>/`
# resolving locally.
#
# `lib.unique` collapses any duplicate (e.g. if forge.domain
# happens to equal hyperhiveDomain or matrixCfg.gatewayHost) so
# `/etc/hosts` doesn't carry the same entry twice.
# `/etc/hosts` entries for local dev — bare hive domain + any
# sub-domain modules that are on. `lib.unique` dedupes if any
# sub-domain happens to equal another. See `docs/gateway.md`
# ("Local dev").
networking.hosts = lib.mkIf (cfg.localHostsEntry && hyperhiveDomain != null) {
"127.0.0.1" = lib.unique (
[ hyperhiveDomain ]

View file

@ -9,50 +9,20 @@ let
hyperhiveDomain = config.services.hyperhive.domain;
effectiveServerName = if cfg.serverName != null then cfg.serverName else hyperhiveDomain;
# Three files are missing from nixpkgs's `pkgs.fluffychat-web` dist
# because `flutter341.buildFlutterApplication` doesn't run the dart
# web-worker compile pass + doesn't run the native_imaging package's
# emscripten build (#685):
#
# - native_executor.js ← flutter web worker entry, compiled
# from web/native_executor.dart via
# `dart compile js` (handled inline
# in `fluffychat-web-fixed.postInstall`
# below — dart SDK is already in the
# flutter341 closure)
#
# - Imaging.js / Imaging.wasm ← emscripten-compiled C library from
# the native_imaging dart package
# (vendored by Famedly). The package
# ships C source + a Makefile that
# builds them via emcc; nixpkgs's
# flutter builder doesn't run that
# pipeline. Built from source via
# `fluffychat-web-imaging` below
# (per mara's #685 call: "fix the
# compile … dont use the prebuilt
# binary").
#
# When `flutter341.buildFlutterApplication` grows worker + emcc
# support upstream, drop both this derivation and the postInstall.
# Three files are missing from `pkgs.fluffychat-web` because
# `flutter341.buildFlutterApplication` doesn't run the dart
# web-worker compile pass + doesn't run the native_imaging emscripten
# build (#685). `fluffychat-web-imaging` below builds the latter from
# source via `pkgs.emscripten`; the worker compile is inline in
# `fluffychat-web-fixed.postInstall`. Drop both when nixpkgs's
# flutter builder grows worker + emcc support upstream.
# Imaging.{js,wasm} built from source: native_imaging's `js/Makefile`
# runs `emcmake cmake` → `make -C build` → `emcc` to produce the
# emscripten-wrapped C library that fluffychat's main.dart.js
# references at runtime.
#
# Source: the exact native_imaging derivation that `pkgs.fluffychat-web`
# already pulls in via its `pubspecLock` (resolved by nixpkgs's flutter
# pub-cache machinery), reached via `passthru.pubspecLock.dependencySources`.
# This means **no parallel hash pin** — when nixpkgs bumps
# `pkgs.fluffychat-web` (and with it the pubspec.lock-resolved
# native_imaging version), our build automatically picks up the
# matching source. Version is also pulled from passthru for the
# derivation's `version` attr so it stays in lockstep.
#
# Closure cost: `pkgs.emscripten` is ~3.6 GiB build-time (LLVM +
# toolchain). Runtime closure is only the two produced files —
# nothing emscripten-shaped survives into the deployed dist.
# `Imaging.{js,wasm}` built from `native_imaging`'s C source via
# emscripten. Source comes from
# `pkgs.fluffychat-web.passthru.pubspecLock.dependencySources` so
# there's no parallel hash pin — version auto-syncs with nixpkgs
# bumps. Build closure +~3.6 GiB (emscripten LLVM); runtime closure
# is just the two output files.
fluffychat-web-imaging = pkgs.stdenv.mkDerivation {
pname = "fluffychat-web-imaging";
version = pkgs.fluffychat-web.passthru.pubspecLock.dependencyVersions.native_imaging;
@ -102,18 +72,10 @@ let
};
};
# `pkgs.fluffychat-web` with #685's three missing files patched
# in via postInstall, plus the existing `--base-href "/matrix/"`
# override (#634) for the sub-path mount.
# `pkgs.fluffychat-web` with #685's three missing files patched in
# via postInstall. Mount point is `matrix.<hive>/` (#772); upstream
# `--base-href "/"` is correct at sub-domain root, no override.
fluffychat-web-fixed = pkgs.fluffychat-web.overrideAttrs (old: {
# `--base-href "/matrix/"` so relative asset paths resolve
# under the sub-path mount (#634). Upstream default is `/`,
# wrong for hyperhive's `/matrix/` location.
flutterBuildFlags = (old.flutterBuildFlags or [ ]) ++ [
"--base-href"
"/matrix/"
];
# `dart` from the flutter341 closure (already pulled, no
# incremental closure cost) so we can compile the web-worker
# entry point that buildFlutterApplication skips.
@ -122,34 +84,14 @@ let
postInstall =
(old.postInstall or "")
+ ''
# #685: compile web/native_executor.dart → native_executor.js.
# The flutter web bootstrap loads this from /matrix/native_executor.js
# at startup; without it, main.dart.js logs a network-error and
# the SPA renders blank (see #643 for the symptom).
#
# `dart compile js` needs `.dart_tool/package_config.json` to
# resolve `package:matrix/...` and the rest of fluffychat's
# `pubspec.lock` deps. buildFlutterApplication's pub-get step
# writes that file to the build CWD (the unpacked source dir),
# not to `$src` (the read-only nix store path). So we must use
# a relative path that walks up from `web/` to the build CWD
# where pub-get's package_config lives — pointing at
# `$src/web/native_executor.dart` walks up to `$src/`, finds
# no `.dart_tool/`, and fails with `Couldn't resolve the
# package 'matrix'` (mara's first build attempt on #685).
#
# nixpkgs's buildFlutterApplication leaves CWD at the source
# root for postInstall (see `pkgs/development/compilers/flutter/
# build-support/build-flutter-application.nix` — installPhase
# is `cp -r build/web "$out"` with no `cd` first). So `web/...`
# resolves correctly here.
# `web/...` is relative to build CWD so dart's package_config
# walk-up hits buildFlutterApplication's pub-get output (#685
# / #733 fixup — `$src/web/...` would walk up to a read-only
# store path with no `.dart_tool/`).
${pkgs.flutter341.dart}/bin/dart compile js \
-o $out/native_executor.js \
web/native_executor.dart
# #685: install Imaging.{js,wasm} built from the native_imaging
# dart package's C source via emscripten (see
# `fluffychat-web-imaging` above for the build-time rationale).
install -m 644 ${fluffychat-web-imaging}/Imaging.js $out/Imaging.js
install -m 644 ${fluffychat-web-imaging}/Imaging.wasm $out/Imaging.wasm
'';
@ -263,49 +205,17 @@ in
'';
example = "matrix.example.com";
description = ''
Public hostname for the matrix homeserver behind the
hive-gateway nginx (#747, mara verdict on #749:9609 — sub-domain
over sub-path for matrix, but **not user-visible** because the
`.well-known/matrix/{client,server}` redirect routes clients
through automatically).
Public hostname for the matrix homeserver behind the gateway.
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`). See
`docs/gateway.md` for the vhost map + matrix discovery flow,
and the federation port-8448 caveat at the bottom of that doc.
When set + gateway is on, the gateway adds a `server { server_name
= gatewayHost; }` block that proxies `/_matrix/...`
`http://127.0.0.1:''${httpPort}/_matrix/...`. The
`.well-known/matrix/{client,server}` endpoints (served by the
gateway at the bare hive-domain) then point at
`http(s)://''${gatewayHost}/` matrix clients automatically
discover + follow that delegation.
Defaults to `matrix.''${services.hyperhive.domain}` when the
hive-domain is set (idiomatic matrix-spec shape `matrix`
labelled under the hive's bare server_name domain). Defaults to
`null` when the hive-domain is unset (gateway vhost not added;
clients reach tuwunel directly on `httpPort`).
Set to a full hostname (`matrix.example.com`,
`homeserver.internal.lan`) for a bespoke vhost shape. Set to
`null` to disable the gateway vhost entirely (tuwunel stays
direct on `httpPort`).
**server_name vs gatewayHost**: `serverName` is the matrix
identifier domain embedded in user/room IDs irrevocably (per
#660 default = bare hive-domain). `gatewayHost` is just where
the API listens behind nginx. The two are different see the
matrix-spec server-discovery flow.
**Federation port caveat**: the `.well-known/matrix/server`
delegation advertises `''${gatewayHost}` with no port suffix
when the gateway listens on 80. Per the matrix federation
spec, peers fall back to port 8448 when no explicit port is
present but the gateway only listens on the configured
`services.hyperhive.gateway.port`. Cross-hive federation
therefore needs either:
- a DNS SRV record (`_matrix._tcp.''${gatewayHost}` port 80),
- or `services.hyperhive.matrix.openFirewall = true` so peers
can reach tuwunel's federation port directly.
Hyperhive is mostly closed/internal, so this rarely bites in
practice but flagging for the federation-curious operator.
Note: `gatewayHost` is the API listener hostname (where nginx
proxies `/_matrix/*`); `serverName` is the matrix-identifier
domain embedded irrevocably in user/room IDs (per #660
default = bare hive-domain). The two are distinct.
'';
};
@ -384,22 +294,12 @@ in
default = cfg.enable;
defaultText = lib.literalExpression "config.services.hyperhive.matrix.enable";
description = ''
Serve a matrix web client (default `pkgs.fluffychat-web`) as
a static dist at `/matrix/` via the hive-gateway nginx
(#607 / #634). Defaults to whatever
`services.hyperhive.matrix.enable` is turning on the
homeserver gives you the web client by default; set to
`false` explicitly to opt out of the GUI while keeping
the homeserver running for agents. Requires
`services.hyperhive.gateway.enable` (default on); when
gateway is off no one hosts the GUI and the
`M4TR1X ` dashboard tab is hidden.
fluffychat-web supports per-login server pick point it at
the in-host tuwunel URL (`http://localhost:8008` by
default) the first time. The post-#15 nginx-front re-root
(`https://matrix.''${services.hyperhive.domain}`) is tracked
separately in #609.
Serve a matrix web client at `matrix.''${services.hyperhive.domain}/`.
Requires `gateway.enable` + `matrix.gatewayHost != null`
(default true / `matrix.<hive>` when hive-domain set). When
off, the dashboard's `M4TR1X ` tab is hidden. See
`docs/gateway.md` for the discovery flow that lets clients
auto-find the sub-domain.
'';
};
@ -407,24 +307,15 @@ in
type = lib.types.package;
default = fluffychat-web-fixed;
defaultText = lib.literalMD ''
`pkgs.fluffychat-web` rebuilt with `--base-href /matrix/` (#634)
and patched via `postInstall` to add the three files
`flutter341.buildFlutterApplication` skips: `native_executor.js`
(compiled via `dart compile js` from `web/native_executor.dart`),
plus `Imaging.js` + `Imaging.wasm` (built from the
`native_imaging` dart package's C source via `pkgs.emscripten`).
See the `let` block in `nix/modules/hive-matrix.nix` for the
full rationale (#685).
`pkgs.fluffychat-web` + #685 `postInstall` patch (adds the
three files `flutter341.buildFlutterApplication` skips).
'';
description = ''
Static web client dist to serve at `/matrix/`. Defaults to
`pkgs.fluffychat-web` rebuilt with `--base-href "/matrix/"`
so relative asset paths resolve under the sub-path mount
(#634), plus a `postInstall` patch for #685's three missing
files. Override to swap for `hydrogen-web` (lightest),
`cinny` (no threads), `element-web` (heaviest, full
features), or an out-of-tree client dist any replacement
also needs its `<base href>` aligned with the mount path.
Static web client dist served at `matrix.<hive>/`. Override
to swap fluffychat for hydrogen-web, cinny, element-web, or
an out-of-tree dist any replacement is mounted at the
sub-domain root with the upstream-default `<base href "/">`,
no sub-path gymnastics needed.
'';
};
};