Skip to content

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 _FILE support, 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.