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.
This commit is contained in:
atlas 2026-05-31 14:17:35 +02:00 committed by mara
commit df549ed2a5
2 changed files with 134 additions and 117 deletions

View file

@ -231,11 +231,14 @@ in
# takes over. # takes over.
# #
# Only emitted when the matrix GUI is on (saves a no-op # Only emitted when the matrix GUI is on (saves a no-op
# `map` directive otherwise). # `map` directive otherwise). Target now points at the
# sub-domain-root `/index.html` (fluffychat moved off
# `<hive>/matrix/` sub-path to `matrix.<hive>/` root in
# #772; `--base-href` reverts to upstream-default `/`).
appendHttpConfig = lib.optionalString (matrixCfg.enable && matrixCfg.gui.enable) '' appendHttpConfig = lib.optionalString (matrixCfg.enable && matrixCfg.gui.enable) ''
map $http_accept $matrix_spa_target { map $http_accept $matrix_spa_target {
default "/__matrix_spa_no_html_fallback"; default "/__matrix_spa_no_html_fallback";
"~*text/html" "/matrix/index.html"; "~*text/html" "/index.html";
} }
''; '';
virtualHosts = { virtualHosts = {
@ -247,67 +250,41 @@ in
} }
]; ];
locations = locations =
# Matrix GUI: when the operator has flipped both # Bare-domain `<hive>/matrix/*` → 301 redirect to the
# `services.hyperhive.matrix.enable` and `matrix.gui.enable` # matrix sub-domain root (#772). fluffychat-web used to
# on, nginx serves fluffychat-web (or whatever override) # live at this sub-path; #772 moved it to `matrix.<hive>/`
# as a static dist at `/matrix/`. # so it gets full sub-domain origin isolation + sub-spec
# matches the matrix-spec deploy shape. The redirect
# preserves bookmark + deep-link compatibility for
# `<hive>/matrix/#/...` URLs during the transition;
# operators can drop the redirect block once it's been
# in the wild long enough that no stale bookmarks remain.
# #
# SPA fallback (iris/#643, rewritten in #686 per mara # Only emitted when both the matrix GUI is on AND
# + damocles on PR #729): the original # `matrixCfg.gatewayHost` is set (else there's no
# `try_files $uri $uri/ /matrix/index.html;` shape # canonical sub-domain to redirect to).
# silently masked missing assets — flutter's bootstrap lib.optionalAttrs (
# requesting e.g. `/matrix/native_executor.js` got matrixCfg.enable
# `index.html` (Content-Type: text/html, status 200) && matrixCfg.gui.enable
# when the file was absent from the dist, so the JS && matrixCfg.gatewayHost != null
# runtime never loaded and `/matrix/` rendered blank ) (
# without any visible error. let
# portSuffix = if cfg.port == 80 then "" else ":${toString cfg.port}";
# The followup #729 narrowed it with an extension target = "http://${matrixCfg.gatewayHost}${portSuffix}";
# allowlist; this version uses `$matrix_spa_target` in
# (defined in the `appendHttpConfig` above, keyed on {
# the `Accept` header) so the decision lives in HTTP # `rewrite ^/matrix/(.*)$ → matrix.<hive>/$1` —
# semantics rather than a maintained extension list. # strips the `/matrix/` prefix on the way out so
# Navigations (Accept: text/html) fall to index.html; # `<hive>/matrix/#/rooms/...` lands at the right
# asset fetches (Accept: */*, image/*, etc.) get a # SPA route on the sub-domain side. `permanent`
# clean 404 via the trailing `=404`. # emits 301 + sets the canonical Location header.
lib.optionalAttrs (matrixCfg.enable && matrixCfg.gui.enable) { "/matrix/" = {
"/matrix/" = { extraConfig = ''
alias = "${matrixCfg.gui.package}/"; rewrite ^/matrix/(.*)$ ${target}/$1 permanent;
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}"}';
'';
};
}
// //
# `.well-known/matrix/*` auto-discovery (#660): when # `.well-known/matrix/*` auto-discovery (#660): when
# `services.hyperhive.matrix.enable` is on and the # `services.hyperhive.matrix.enable` is on and the
@ -507,22 +484,63 @@ in
port = cfg.port; port = cfg.port;
} }
]; ];
locations = { locations =
"/_matrix/" = { {
proxyPass = "http://127.0.0.1:${toString matrixCfg.httpPort}"; "/_matrix/" = {
proxyWebsockets = true; proxyPass = "http://127.0.0.1:${toString matrixCfg.httpPort}";
extraConfig = '' proxyWebsockets = true;
proxy_buffering off; extraConfig = ''
client_max_body_size 50M; proxy_buffering off;
proxy_read_timeout 1h; client_max_body_size 50M;
proxy_send_timeout 1h; proxy_read_timeout 1h;
add_header Access-Control-Allow-Origin *; proxy_send_timeout 1h;
''; add_header Access-Control-Allow-Origin *;
}; '';
"/" = { };
return = "404"; }
}; //
}; # Fluffychat-web (or override) served at sub-domain
# root (#772 — moved from bare-domain `/matrix/`).
# nginx location-precedence: longer prefix wins,
# so `/_matrix/` (above) handles the matrix API
# and `/` falls through to fluffychat for
# everything else.
#
# SPA fallback uses the same Accept-header `$matrix_spa_target`
# map from `appendHttpConfig` above — navigations
# fall to `/index.html` (now sub-domain-root path),
# asset misses return clean 404.
#
# `= /config.json` serves the FluffyChat boot-config
# pre-fill with the operator's hive-domain so the
# client's `.well-known/matrix/client` lookup hits
# the right delegation endpoint (#736).
lib.optionalAttrs (matrixCfg.gui.enable) (
{
"/" = {
alias = "${matrixCfg.gui.package}/";
extraConfig = ''
try_files $uri $uri/ $matrix_spa_target =404;
'';
};
}
// lib.optionalAttrs (hyperhiveDomain != null) {
"= /config.json" = {
extraConfig = ''
default_type application/json;
return 200 '{"defaultHomeserver":"${hyperhiveDomain}"}';
'';
};
}
)
//
# Fall-through `/` when GUI is off: nothing else
# lives at the matrix vhost, return 404 cleanly.
lib.optionalAttrs (!matrixCfg.gui.enable) {
"/" = {
return = "404";
};
};
}; };
}; };
}; };

View file

@ -103,17 +103,14 @@ let
}; };
# `pkgs.fluffychat-web` with #685's three missing files patched # `pkgs.fluffychat-web` with #685's three missing files patched
# in via postInstall, plus the existing `--base-href "/matrix/"` # in via postInstall. No `--base-href` override anymore (#772 moves
# override (#634) for the sub-path mount. # fluffychat from the bare-domain `<hive>/matrix/` sub-path to the
# `matrix.<hive>/` sub-domain root; upstream's default `--base-href
# "/"` is correct at sub-domain root). The previous `/matrix/`
# override from #634 + #754 (forge sub-domain pattern) lived here
# until the matrix sub-domain vhost in #764 made the sub-path
# redundant.
fluffychat-web-fixed = pkgs.fluffychat-web.overrideAttrs (old: { 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 # `dart` from the flutter341 closure (already pulled, no
# incremental closure cost) so we can compile the web-worker # incremental closure cost) so we can compile the web-worker
# entry point that buildFlutterApplication skips. # entry point that buildFlutterApplication skips.
@ -385,21 +382,22 @@ in
defaultText = lib.literalExpression "config.services.hyperhive.matrix.enable"; defaultText = lib.literalExpression "config.services.hyperhive.matrix.enable";
description = '' description = ''
Serve a matrix web client (default `pkgs.fluffychat-web`) as Serve a matrix web client (default `pkgs.fluffychat-web`) as
a static dist at `/matrix/` via the hive-gateway nginx a static dist at the root of the matrix sub-domain vhost
(#607 / #634). Defaults to whatever (`matrix.''${services.hyperhive.domain}/`, via the hive-gateway
nginx #607 / #634 / #772). Defaults to whatever
`services.hyperhive.matrix.enable` is turning on the `services.hyperhive.matrix.enable` is turning on the
homeserver gives you the web client by default; set to homeserver gives you the web client by default; set to
`false` explicitly to opt out of the GUI while keeping `false` explicitly to opt out of the GUI while keeping the
the homeserver running for agents. Requires homeserver running for agents. Requires
`services.hyperhive.gateway.enable` (default on); when `services.hyperhive.gateway.enable` (default on) +
gateway is off no one hosts the GUI and the `services.hyperhive.matrix.gatewayHost != null` (default
`M4TR1X ` dashboard tab is hidden. `matrix.<hive>`); when either 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 `.well-known/matrix/{client,server}` delegation (served
the in-host tuwunel URL (`http://localhost:8008` by on the bare hive-domain) advertises this sub-domain, so
default) the first time. The post-#15 nginx-front re-root clients opened at `http://<hive>/` auto-discover the
(`https://matrix.''${services.hyperhive.domain}`) is tracked fluffychat root automatically.
separately in #609.
''; '';
}; };
@ -407,24 +405,25 @@ in
type = lib.types.package; type = lib.types.package;
default = fluffychat-web-fixed; default = fluffychat-web-fixed;
defaultText = lib.literalMD '' defaultText = lib.literalMD ''
`pkgs.fluffychat-web` rebuilt with `--base-href /matrix/` (#634) `pkgs.fluffychat-web` patched via `postInstall` to add the
and patched via `postInstall` to add the three files three files `flutter341.buildFlutterApplication` skips:
`flutter341.buildFlutterApplication` skips: `native_executor.js` `native_executor.js` (compiled via `dart compile js` from
(compiled via `dart compile js` from `web/native_executor.dart`), `web/native_executor.dart`), plus `Imaging.js` +
plus `Imaging.js` + `Imaging.wasm` (built from the `Imaging.wasm` (built from the `native_imaging` dart
`native_imaging` dart package's C source via `pkgs.emscripten`). package's C source via `pkgs.emscripten`). See the `let`
See the `let` block in `nix/modules/hive-matrix.nix` for the block in `nix/modules/hive-matrix.nix` for the full
full rationale (#685). rationale (#685, #772).
''; '';
description = '' description = ''
Static web client dist to serve at `/matrix/`. Defaults to Static web client dist to serve at the matrix sub-domain
`pkgs.fluffychat-web` rebuilt with `--base-href "/matrix/"` root (`matrix.''${services.hyperhive.domain}/`). Defaults
so relative asset paths resolve under the sub-path mount to `pkgs.fluffychat-web` with the #685 `postInstall` patch
(#634), plus a `postInstall` patch for #685's three missing for the three missing files. Override to swap for
files. Override to swap for `hydrogen-web` (lightest), `hydrogen-web` (lightest), `cinny` (no threads),
`cinny` (no threads), `element-web` (heaviest, full `element-web` (heaviest, full features), or an out-of-tree
features), or an out-of-tree client dist any replacement client dist any replacement gets served at the sub-domain
also needs its `<base href>` aligned with the mount path. root with the upstream-default `<base href "/">`, no
sub-path gymnastics needed.
''; '';
}; };
}; };