Matrix chat add-on
Waldur ships an optional Matrix chat integration (the matrix_chat Django app and the homeport src/matrix/ views). When the add-on is enabled, project members can chat in per-project Matrix rooms; with the matrix-rtc sub-profile they can also start voice/video calls from the chat.
The add-on bundles a Tuwunel homeserver (the same image used by the upstream dev stack) wired into the Caddy reverse proxy on the same domain — no extra DNS, no federation port. Tokens are auto-generated on first start and persisted in a Docker volume.
Activation
Chat only:
1 | |
Chat + voice/video calls:
1 | |
The matrix-rtc profile requires matrix because lk-jwt-service shares Tuwunel's network namespace. Activating it on its own will fail.
Pinned image tags
Matrix component versions live in .env. Bump them deliberately:
| Variable | Default | Component |
|---|---|---|
WALDUR_TUWUNEL_IMAGE_TAG |
v1.9.0 |
Tuwunel homeserver — requires 1.7.0+ for Synapse-compatible registration_shared_secret. Kept in lockstep with the Helm chart's matrixChat.homeserver.imageTag — one supported homeserver version across both packaging paths |
WALDUR_LIVEKIT_IMAGE_TAG |
v1.13.7 |
LiveKit SFU |
WALDUR_LK_JWT_IMAGE_TAG |
0.7.0 |
lk-jwt-service, 0.4.0 or newer: homeport requests call tokens from its /get_token endpoint. Requires explicit LIVEKIT_FULL_ACCESS_HOMESERVERS (auto-set) |
All three publish multi-arch (linux/amd64 and linux/arm64) manifests for the pinned tags. Re-check with docker buildx imagetools inspect <image>:<tag> after bumps.
Back up before bumping Tuwunel. It migrates its embedded database in place
on the first boot of a new version, before it listens and without logging
anything, so read the
upstream release notes
before moving in either direction. A tuwunel container that is up but not
answering /_matrix/client/versions is migrating, not hung: do not restart it,
a restart mid-migration corrupts the database.
Downgrades are the dangerous direction. An older Tuwunel boots cleanly on a
migrated database and then silently serves stale data from the old stores. Stop
the stack and back up the tuwunel_data volume before changing the tag;
restoring that backup is the only rollback. The Helm chart's
Matrix chat guide
has the upgrade and CVE-response procedure, which applies to both packaging
paths.
WALDUR_DOMAIN is frozen once the homeserver has data. It is the Matrix
server_name, baked into every user and room ID. From 1.9.0 the homeserver
also stamps it into the database on first boot and refuses to start under a
different name:
1 | |
That is not a bug to work around by wiping tuwunel_data — wiping it discards
the whole chat corpus. Changing the domain means a fresh homeserver.
One-time appservice registration
Tuwunel does not load appservice descriptors from a file — it requires registration via the !admin appservices register admin-room command. The waldur-matrix-init container renders a ready-to-paste descriptor into the waldur_matrix_secrets volume; do the following once after the first --profile matrix up -d.
The default config has WALDUR_MATRIX_OPEN_REGISTRATION=false, so client-side registration (Element Web sign-up form) is disabled. Use Tuwunel's Synapse-compatible admin endpoint (HMAC-keyed by the registration secret) to provision the admin user. The snippet below does the whole thing — create admin, log in, find the auto-joined admin room, post the !admin appservices register message with the descriptor:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | |
The bot then becomes @waldur-bot:<your-domain> and can post on Waldur's behalf.
Prefer Element Web? Set WALDUR_MATRIX_OPEN_REGISTRATION=true in .env before the first --profile matrix up -d, then register the admin user via the Element Web sign-up form using REG_TOKEN from the secrets volume. Switch the flag back to false afterwards (re-render takes effect on the next --profile matrix up -d).
Enabling the homeport UI
Backend access to Matrix is gated by the MATRIX_ENABLED Constance flag (auto-set by waldur-matrix-init). The homeport UI is gated separately by a feature flag — enable it once via the load_features management command:
1 2 | |
After a hard reload (Cmd-Shift-R / Ctrl-Shift-R), project views show the Communication tab — but only after a Matrix room has been created for that project (the tab is gated by hasActiveProjectMatrixRoomInCache). Until then, the room-creation entry point lives at Manage → Chat (?tab=chat query parameter — direct paths like /manage/chat/ return 404).
Token rotation
To rotate AS/HS tokens (e.g., after credential exposure):
1 2 3 | |
On re-up, waldur-matrix-init generates fresh tokens, re-renders the descriptor, and re-seeds Constance. Re-run the one-time appservice registration step above — Tuwunel still holds the old descriptor until you re-register, and the bot will fail with M_UNKNOWN_TOKEN in the meantime. The room database in tuwunel_data is untouched, so existing rooms survive.
LiveKit / voice & video notes
WALDUR_LIVEKIT_NODE_IP advertises the host's RTC media address to clients. The default 127.0.0.1 is correct for a local demo only — for any reachable deployment, set this to the host's external IP or DNS name so remote clients can connect. The RTC media ports (WALDUR_MATRIX_RTC_TCP_PORT/UDP_PORT, default 7881/7882) must also be reachable from clients.
WALDUR_LIVEKIT_KEY / WALDUR_LIVEKIT_SECRET default to development values. Override both for anything beyond a localhost demo. Use a secret of at least 32 characters; LiveKit logs an error at startup for anything shorter.
Calls need a WALDUR_DOMAIN that resolves to the host from inside containers. lk-jwt-service shares Tuwunel's network namespace and verifies each caller's Matrix OpenID token with a federation lookup of the homeserver named WALDUR_DOMAIN. With WALDUR_DOMAIN=localhost that lookup dials localhost:443 and localhost:8448 inside the namespace, where nothing listens, so every call token request fails and calls never start. Chat is unaffected. Use a DNS name that points at the host, or host.docker.internal for a local demo on Docker Desktop. Choose it before the first start, because WALDUR_DOMAIN is frozen once the homeserver has data (see Pinned image tags above).
TURN relay (symmetric NAT / iCloud Private Relay)
Direct media to WALDUR_LIVEKIT_NODE_IP fails for clients behind symmetric NAT or proxies that don't carry WebRTC UDP. Enabling TURN gives LiveKit a relay those clients fall back to.
Set WALDUR_MATRIX_TURN_ENABLED=true (within --profile matrix-rtc). The stack uses TURN/UDP on port 443 — udp_port is the port LiveKit advertises to clients, and 443 is the firewall-friendly choice. Because the stack is single-host, the relay lives at WALDUR_DOMAIN:443/udp — the same IP that already serves the web UI, so no extra DNS record is needed.
This mode needs no certificate: TURN/UDP isn't TLS, so there is nothing to provision or renew. It also doesn't collide with Caddy — Caddy binds 443/tcp (HTTPS) while TURN takes 443/udp, which is otherwise free on the host.
Open 443/udp on any upstream firewall / cloud security group (the same way 443/tcp is already opened for the web UI).
1 2 | |
Coverage note: TURN/UDP rescues symmetric-NAT clients and any network that permits outbound UDP on 443. It does not help clients on networks that block all outbound UDP (strict corporate firewalls) — that requires TURN/TLS on TCP 443, which on a single host means giving LiveKit its own IP or a layer-4 load balancer (the helm topology). For a single-host compose deployment, TURN/UDP is the pragmatic option.
Apple Silicon
The Matrix component images already publish linux/arm64. The Waldur images may need a local arm64 rebuild because of openportal's native dependency:
1 2 | |
Then in .env: WALDUR_MASTERMIND_IMAGE_TAG=local-arm, WALDUR_HOMEPORT_IMAGE_TAG=local-arm, DOCKER_REGISTRY_PREFIX=. See the existing "Apple Silicon caveats" guidance for QEMU fallback details.
Verifying the add-on
End-to-end smoke after docker compose --profile matrix up -d:
1 2 3 4 5 6 7 8 9 | |
After completing the one-time appservice registration above, visit https://${WALDUR_DOMAIN}/projects/<uuid>/manage/?tab=chat as a staff user and click Create chat room. The Manage tabs use query-param URLs (?tab=chat), not path segments — direct paths like /manage/chat/ 404.
Before the registration is pasted, room creation fails with M_UNKNOWN_TOKEN in docker compose logs waldur-mastermind-worker — that is expected and is the signal that Tuwunel still needs the appservice descriptor.
Troubleshooting
M_UNKNOWN_TOKENin worker logs after a token rotation: re-run the one-time appservice registration step. The descriptor Tuwunel has is stale.- Webhook
DisallowedHosterrors: the appservice descriptor is rendered withurl: http://waldur-mastermind-api:8080(the Compose service name), which is inALLOWED_HOSTSfor the dockerised settings. If you change the URL — for example to call back via an external hostname — patchALLOWED_HOSTSinconfig/waldur-mastermind/override.conf.py. - Browser chat drawer fails to connect: the backend talks to Tuwunel internally at
http://tuwunel.internal:6167(Docker DNS); the browser must reach Tuwunel through Caddy athttps://${WALDUR_DOMAIN}.waldur-matrix-initseeds both — backend usesMATRIX_HOMESERVER_URL, browser-facing endpoints serveMATRIX_HOMESERVER_PUBLIC_URL(requireswaldur-mastermind>= 8.x with the dual-URL split). If the chat drawer logs CSP errors connecting totuwunel.internal, verifyMATRIX_HOMESERVER_PUBLIC_URLis set:docker exec waldur-mastermind-worker waldur shell -c "from constance import config; print(config.MATRIX_HOMESERVER_PUBLIC_URL)". - A call shows "Could not connect to the call.": confirm
--profile matrix-rtcis active, then check the call token request tohttps://${WALDUR_DOMAIN}/lk-jwt/…in the browser's network tab anddocker compose logs lk-jwt-service. Common causes:WALDUR_DOMAIN=localhost(see the LiveKit notes above); aWALDUR_DOMAINmismatch withLIVEKIT_FULL_ACCESS_HOMESERVERS; or400 Missing room parameterfrom/lk-jwt/sfu/get, which means the homeport image still posts to lk-jwt's legacy endpoint while lk-jwt is 0.6.0 or newer. KeepWALDUR_HOMEPORT_IMAGE_TAGandWALDUR_LK_JWT_IMAGE_TAGat the versions.env.examplepins. - Diagnostics shows "Public homeserver reachable" as FAIL even though the chat works: the reachability probe at
/api/admin/matrix/diagnostics/runs from inside the mastermind container. The public URL (https://${WALDUR_DOMAIN}) is a Caddy-proxied address reachable from the browser, not from the backend's network namespace — so the probe getsConnection refused. The "Public homeserver URL configured" check above it confirms the value is set; verify the chat round-trips end-to-end from a browser instead of trusting this single probe. - Communication tab missing on a project: requires three things — the
project.show_matrix_chatfeature flag is on, a Matrix room exists for the project, AND the room cache has populated. The third only happens after the project view is visited at least once in the current session. If you navigate directly to/projects/<uuid>/communication/and get 404, visit/projects/<uuid>/first, then the tab appears in the nav.