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.
#
# 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) ''
map $http_accept $matrix_spa_target {
default "/__matrix_spa_no_html_fallback";
"~*text/html" "/matrix/index.html";
"~*text/html" "/index.html";
}
'';
virtualHosts = {
@ -247,67 +250,41 @@ 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/`.
# Bare-domain `<hive>/matrix/*` → 301 redirect to the
# matrix sub-domain root (#772). fluffychat-web used to
# live at this sub-path; #772 moved it to `matrix.<hive>/`
# 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
# + 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}"}';
'';
};
}
# Only emitted when both the matrix GUI is on AND
# `matrixCfg.gatewayHost` is set (else there's no
# canonical sub-domain to redirect to).
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
{
# `rewrite ^/matrix/(.*)$ → matrix.<hive>/$1` —
# strips the `/matrix/` prefix on the way out so
# `<hive>/matrix/#/rooms/...` lands at the right
# SPA route on the sub-domain side. `permanent`
# emits 301 + sets the canonical Location header.
"/matrix/" = {
extraConfig = ''
rewrite ^/matrix/(.*)$ ${target}/$1 permanent;
'';
};
}
)
//
# `.well-known/matrix/*` auto-discovery (#660): when
# `services.hyperhive.matrix.enable` is on and the
@ -507,22 +484,63 @@ 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 *;
'';
};
"/" = {
return = "404";
};
};
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 *;
'';
};
}
//
# 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
# in via postInstall, plus the existing `--base-href "/matrix/"`
# override (#634) for the sub-path mount.
# in via postInstall. No `--base-href` override anymore (#772 moves
# 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: {
# `--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.
@ -385,21 +382,22 @@ in
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
a static dist at the root of the matrix sub-domain vhost
(`matrix.''${services.hyperhive.domain}/`, via the hive-gateway
nginx #607 / #634 / #772). 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.
`false` explicitly to opt out of the GUI while keeping the
homeserver running for agents. Requires
`services.hyperhive.gateway.enable` (default on) +
`services.hyperhive.matrix.gatewayHost != null` (default
`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 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.
The `.well-known/matrix/{client,server}` delegation (served
on the bare hive-domain) advertises this sub-domain, so
clients opened at `http://<hive>/` auto-discover the
fluffychat root automatically.
'';
};
@ -407,24 +405,25 @@ 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` 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, #772).
'';
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 to serve at the matrix sub-domain
root (`matrix.''${services.hyperhive.domain}/`). Defaults
to `pkgs.fluffychat-web` with the #685 `postInstall` patch
for the 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 gets served at the sub-domain
root with the upstream-default `<base href "/">`, no
sub-path gymnastics needed.
'';
};
};