Matrix (Synapse + MAS + Bridges)¶
Stack:
stack/matrix/· Host:phil-app· Updated: 2026-02-28
Matrix homeserver (server_name: philipp.info) with MAS authentication delegated to Keycloak, and 5 mautrix bridges.
Overview¶
Auth is fully delegated to Matrix Authentication Service (MAS) which delegates upstream to Keycloak (sso.philipp.info/realms/family). Native OIDC, LDAP auth, and shared_secret_authenticator have been removed from Synapse.
Architecture¶
Containers¶
- Synapse — Matrix homeserver
- MAS — Matrix Authentication Service (auth delegation, runs at
matrix.philipp.info/auth/) - db — PostgreSQL (Synapse + MAS)
- mautrix-signal (Go megabridge, native signalmeow — signald daemon removed)
- mautrix-whatsapp (Go megabridge)
- mautrix-telegram (Python)
- mautrix-discord (Go)
- mautrix-meta (Go megabridge, replaces deprecated mautrix-facebook)
- doublepuppet appservice — shared double-puppeting for all bridges
Networks¶
compose-matrix-network(stack-internal)traefik-ingress(HTTP routing for Synapse + MAS)ldap-network(MAS → Keycloak needs LDAP? No — MAS uses OIDC to Keycloak directly)
Authentication Flow¶
Matrix Client → Synapse → MAS (matrix.philipp.info/auth/)
↓
Keycloak (sso.philipp.info/realms/family)
Legacy login endpoints (/_matrix/client/*/login, /logout, /refresh) are routed to MAS via Traefik for compatibility with older clients.
Bridge Double-Puppeting¶
All 5 bridges use MSC4190 appservice login for E2EE (org.matrix.msc4190: true in registration files). A dedicated doublepuppet appservice registration distributes the as_token to all bridges via double_puppet.secrets or login_shared_secret_map with as_token: prefix.
Database (PostgreSQL)¶
- Container:
matrix-db(PostgreSQL) - Databases:
synapse,mas(created manually — init-script only runs on first start) - Secrets:
POSTGRES_PASSWORD_FILE=/run/secrets/postgres_password(native_FILEsupport, see ADR-001)
Configuration¶
philipp.info well-known routing¶
server_name is philipp.info but Matrix APIs are on matrix.philipp.info. Some clients (Element X) try philipp.info/_matrix/client/versions on port 443 before checking .well-known.
Traefik router: Host('philipp.info') && PathPrefix('/_matrix') → Synapse. Without this, requests 404 to Friendica.
Operations¶
Health Check¶
sudo docker compose -f /opt/docker/stack/matrix/docker-compose.yml ps
# MAS health
sudo docker exec matrix-mas-1 mas-cli doctor --config /config/config.yaml
# Synapse check
curl -s https://matrix.philipp.info/_matrix/client/versions | jq '.versions[-1]'
Backup¶
Borgmatic config: borgmatic.d/matrix.yaml. Backs up PostgreSQL database dump + media store.
Retention: 2 hourly / 7 daily / 4 weekly / 6 monthly.
Heartbeat: Uptime Kuma push monitor "Backup - Matrix".
Common Commands¶
# MAS diagnostic
sudo docker exec matrix-mas-1 mas-cli doctor --config /config/config.yaml
# syn2mas check (migration verification)
sudo docker exec matrix-mas-1 mas-cli syn2mas check \
--config /config/config.yaml \
--synapse-config /synapse-config/homeserver.yaml
Pitfalls¶
MAS DB must be created manually (PostgreSQL init-script caveat)¶
The create-multiple-postgres-databases.sh init-script only runs when PGDATA is empty (first start). Adding mas to POSTGRES_MULTIPLE_DATABASES has no effect on an existing database.
Fix:
docker exec matrix-db psql -U postgres -c "CREATE USER mas; ALTER USER mas WITH PASSWORD '<pw>'; CREATE DATABASE mas; GRANT ALL PRIVILEGES ON DATABASE mas TO mas;"
docker exec matrix-db psql -U postgres -d mas -c "ALTER SCHEMA public OWNER TO mas;"
syn2mas needs access to homeserver.yaml¶
mas-cli syn2mas needs both the MAS config and the Synapse homeserver.yaml. The MAS container mounts ./config/matrix:/synapse-config:ro for this purpose.
MAS secret must match between config files¶
The matrix.secret in MAS config.yaml must match the secret in Synapse's matrix_authentication_service config block. A mismatch causes 403 errors on /_synapse/mas/ endpoints. Use mas-cli doctor to verify.
shared_secret_authenticator is incompatible with MAS¶
Synapse rejects password auth provider callbacks when matrix_authentication_service is enabled. The shared_secret_authenticator.py module must be removed (modules: []). Bridge double-puppeting uses a dedicated appservice registration (doublepuppet.yaml) instead.
Bridge registration files exist in two places¶
Each bridge has registration.yaml in its own config dir AND a copy in the Synapse config dir (config/matrix/{bridge}.yaml). Both must be in sync — Synapse reads its own copy. Missing org.matrix.msc4190: true in the Synapse-side copy causes bridges to crash with M_UNRECOGNIZED on /_matrix/client/v3/login.
MAS container has no curl/wget — healthcheck must be disabled¶
The MAS Docker image is minimal (distroless-like). mas-cli health does not exist. Set healthcheck: disable: true in docker-compose.
philipp.info needs /_matrix routed on port 443¶
Some clients (Element X) try philipp.info/_matrix/client/versions on port 443 before checking .well-known. Without a Traefik router for Host('philipp.info') && PathPrefix('/_matrix') on websecure, these requests 404 to Friendica.
MAS upstream account linking for migrated users¶
After syn2mas migration with --ignore-missing-auth-providers, existing users are not linked to the Keycloak upstream provider. First login fails with "username already taken".
Fix: Set on_conflict: set in MAS upstream_oauth2.claims_imports.localpart config.