IMPORTANT: This setup uses Matrix Authentication Service (MAS) for authentication. As of Synapse 1.140.0, bridge encryption is NOT compatible with MAS due to missing appservice login support. All bridge configurations in this guide have encryption disabled.
Mautrix bridges have a chicken-and-egg problem during initial setup:
- Bridges generate registration.yaml files on first successful startup
- Synapse needs registration.yaml files to load appservice configuration
- Bridges depend on Synapse being healthy to start (docker-compose dependency)
- If Synapse tries to load non-existent registrations, it crashes
- Crashed Synapse prevents bridges from starting → registration files never get created
The bridges MUST be set up in this specific order:
1. Start bridges WITHOUT Synapse loading them
↓
2. Bridges generate registration.yaml files
↓
3. Configure Synapse to load registration files
↓
4. Restart Synapse with appservice support
↓
5. Bridges can now communicate with Synapse
Use the provided setup-bridges.sh script which handles the correct sequence:
#!/bin/bash
# This script must be run AFTER initial deployment
./setup-bridges.sh- Generates a double puppet appservice token pair
- Starts all bridges simultaneously to generate default configs, then stops them
- Configures each bridge (homeserver address, domain, database, permissions, double puppet, encryption off)
- Sets
hostname: 0.0.0.0so Synapse can reach bridges across Docker containers - Restarts bridges to generate
registration.yamlfiles, then stops them - Sets file permissions so Synapse can read the registration files
- Creates bridge databases in PostgreSQL
- Registers all appservices in
synapse/data/homeserver.yaml - Restarts Synapse, then starts bridges
Telegram requires API credentials from my.telegram.org. Add to .env before running the script, otherwise Telegram is skipped:
TELEGRAM_API_ID=your_api_id
TELEGRAM_API_HASH=your_api_hash
Remove any existing bridge registration references:
# Edit synapse/data/homeserver.yaml
# Remove or comment out:
# app_service_config_files:
# - /bridges/whatsapp/config/registration.yaml
# - /bridges/signal/config/registration.yaml
# Restart Synapse
sudo docker compose -f docker-compose.local.yml restart synapseEdit docker-compose.local.yml and remove depends_on: synapse from bridges:
mautrix-telegram:
# ... other config ...
# TEMPORARILY COMMENT OUT:
# depends_on:
# synapse:
# condition: service_healthy# Start each bridge independently
sudo docker compose -f docker-compose.local.yml up -d mautrix-telegram
sleep 15 # Wait for config generation
sudo docker compose -f docker-compose.local.yml up -d mautrix-whatsapp
sleep 15
sudo docker compose -f docker-compose.local.yml up -d mautrix-signal
sleep 15For each bridge, edit the config file:
Telegram (bridges/telegram/config/config.yaml):
homeserver:
address: http://synapse:8008
domain: matrix.example.test
appservice:
address: http://mautrix-telegram:29317
database: postgres://synapse:PASSWORD@postgres/telegram
bridge:
permissions:
'matrix.example.test': admin
# Bridge encryption disabled - incompatible with MAS
# See "Known Issue: Encrypted Bridges with MAS" section below
encryption:
allow: false
default: falseWhatsApp (bridges/whatsapp/config/config.yaml):
homeserver:
address: http://synapse:8008
domain: matrix.example.test # Make sure this line has a value!
appservice:
address: http://mautrix-whatsapp:29318
database:
uri: postgres://synapse:PASSWORD@postgres/whatsapp?sslmode=disable
bridge:
permissions:
"matrix.example.test": admin
# Bridge encryption disabled - incompatible with MAS
# See "Known Issue: Encrypted Bridges with MAS" section below
encryption:
allow: false
default: falseSignal (bridges/signal/config/config.yaml):
homeserver:
address: http://synapse:8008
domain: matrix.example.test
appservice:
address: http://mautrix-signal:29319
database:
uri: postgres://synapse:PASSWORD@postgres/signal?sslmode=disable
bridge:
permissions:
"matrix.example.test": admin
# Bridge encryption disabled - incompatible with MAS
# See "Known Issue: Encrypted Bridges with MAS" section below
encryption:
allow: false
default: falseStep 5: Create Bridge Databases
sudo docker exec matrix-postgres psql -U synapse -c "CREATE DATABASE telegram;"
sudo docker exec matrix-postgres psql -U synapse -c "CREATE DATABASE whatsapp;"
sudo docker exec matrix-postgres psql -U synapse -c "CREATE DATABASE signal;"sudo docker compose -f docker-compose.local.yml restart mautrix-telegram mautrix-whatsapp mautrix-signal
sleep 20ls -la bridges/*/config/registration.yaml
# You should see:
# bridges/whatsapp/config/registration.yaml
# bridges/signal/config/registration.yaml
# NOTE: Telegram may not generate registration.yaml - that's OKNote about registration.yaml: The generated files should work as-is. Do not add MSC4190/MSC3202 flags since encryption is disabled in this MAS-based setup.
Edit synapse/data/homeserver.yaml and add:
# At the end of the file
app_service_config_files:
- /bridges/whatsapp/config/registration.yaml
- /bridges/signal/config/registration.yamlEnsure bridge directory is mounted in docker-compose.local.yml:
synapse:
volumes:
- ./synapse/data:/data
- ./bridges:/bridges:ro # Add this line# Recreate Synapse with new volume mount
sudo docker compose -f docker-compose.local.yml up -d synapse
# Restore dependencies in docker-compose.local.yml
# Then restart bridges
sudo docker compose -f docker-compose.local.yml restart mautrix-telegram mautrix-whatsapp mautrix-signalsudo docker compose -f docker-compose.local.yml ps | grep bridge
# All bridges should show "Up" status# Telegram
sudo docker compose -f docker-compose.local.yml logs mautrix-telegram --tail 20
# WhatsApp
sudo docker compose -f docker-compose.local.yml logs mautrix-whatsapp --tail 20
# Signal
sudo docker compose -f docker-compose.local.yml logs mautrix-signal --tail 20Successful startup:
INFO - Starting bridge
INFO - Bridge started successfully
INFO - Listening on port 29318
Common errors and fixes:
homeserver.address not configured→ Fix homeserver.address in config.yamlhomeserver.domain not configured→ Fix homeserver.domain (must have value, not empty)database not configured→ Fix database URI with correct passwordpermissions not configured→ Add domain to bridge.permissionsas_token was not accepted→ Registration not loaded in Synapse yet
Once bridges are running:
- Open Element Web:
https://element.example.test - Start a chat with the bridge bot:
- Telegram:
@telegrambot:matrix.example.test - WhatsApp:
@whatsappbot:matrix.example.test - Signal:
@signalbot:matrix.example.test
- Telegram:
- Follow the bot's instructions to link your account
!tg help # Show commands
!tg login # Start login process
help # Show commands
login # Show QR code
help # Show commands
link # Start linking process
Check logs for specific error:
sudo docker compose logs mautrix-whatsapp --tail 50Common causes:
- Config error - Review config.yaml for typos or missing values
- Database error - Ensure database exists and password is correct
- Synapse not reachable - Check
homeserver.addresspoints tohttp://synapse:8008 - Registration mismatch - as_token in registration.yaml must match Synapse config
- Check bridge is running:
docker compose ps - Check you're logged into bridge: Send
statuscommand to bridge bot - Check Synapse logs:
docker compose logs synapse | grep appservice - Verify registration loaded: Check Synapse startup logs for "Registered application service"
Bridge bot needs admin permissions. In bridge config:
bridge:
permissions:
"your-domain.com": admin # Domain-wide admin
"@you:your-domain.com": admin # Or specific userCRITICAL: As of Synapse 1.140.0, there is a known compatibility issue between encrypted bridges and Matrix Authentication Service (MAS).
- Encrypted bridges require appservice login authentication for MSC4190 device masquerading
- When MAS is enabled, it takes over authentication from Synapse
- MAS does not currently support appservice login authentication
- Result: Encrypted bridges fail with
"homeserver does not support appservice login"error
- Bridge crashes during startup when encryption is enabled
- Error message:
"failed to start Matrix connector: homeserver does not support appservice login" - WhatsApp/Telegram may work (if not using encryption)
- Signal bridge typically fails (often defaults to encryption)
Option 1: Disable encryption in affected bridges
# In bridge config.yaml
encryption:
allow: false
default: false
# Remove or comment out msc4190: trueOption 2: Wait for MAS appservice login support This is actively being developed. Check:
- element-hq/matrix-authentication-service#3206
- Recent Synapse/MAS release notes
Option 3: Disable MAS temporarily If encrypted bridges are critical, you may need to use Synapse's built-in authentication instead of MAS until this is resolved.
While bridge encryption is incompatible with MAS, you can still achieve good message attribution using double puppet with unencrypted Matrix rooms. This is the recommended production-ready workaround.
Double puppet allows bridges to send messages as if they came from your actual Matrix user, rather than from a bot account. This provides:
- Better message attribution (messages appear from you, not a bot)
- Improved user experience
- Works reliably without encryption issues
# Generate tokens for the double puppet appservice
AS_TOKEN=$(openssl rand -hex 32)
HS_TOKEN=$(openssl rand -hex 32)
# Save these for later use
echo "AS_TOKEN: $AS_TOKEN"
echo "HS_TOKEN: $HS_TOKEN"Create appservices/doublepuppet.yaml:
id: doublepuppet
url: null
as_token: "YOUR_AS_TOKEN_HERE"
hs_token: "YOUR_HS_TOKEN_HERE"
sender_localpart: doublepuppet
rate_limited: false
namespaces:
users:
- regex: "@.*:YOUR-DOMAIN.COM"
exclusive: falseReplace:
YOUR_AS_TOKEN_HEREwith the AS_TOKEN you generatedYOUR_HS_TOKEN_HEREwith the HS_TOKEN you generatedYOUR-DOMAIN.COMwith your actual Matrix domain
Set permissions:
chmod 644 appservices/doublepuppet.yamlEdit synapse/data/homeserver.yaml and add:
app_service_config_files:
- /appservices/doublepuppet.yaml
- /bridges/whatsapp/config/registration.yaml
- /bridges/signal/config/registration.yaml
- /bridges/telegram/config/registration.yamlEnsure the appservices directory is mounted in your Synapse container:
synapse:
volumes:
- ./synapse/data:/data
- ./bridges:/bridges:ro
- ./appservices:/appservices:ro # Add this lineWhatsApp (bridges/whatsapp/config/config.yaml):
# mautrix-whatsapp uses top-level sections (megabridge format)
double_puppet:
secrets:
your-domain.com: as_token:YOUR_AS_TOKEN_HERE
encryption:
allow: false
default: false
msc4190: falseSignal (bridges/signal/config/config.yaml):
double_puppet:
secrets:
your-domain.com: as_token:YOUR_AS_TOKEN_HERE
encryption:
allow: false
default: false
msc4190: falseTelegram (bridges/telegram/config/config.yaml):
bridge:
login_shared_secret_map:
your-domain.com: as_token:YOUR_AS_TOKEN_HERE
# Disable encryption (not compatible with MAS)
encryption:
allow: false
default: falseReplace:
your-domain.comwith your Matrix domainYOUR_AS_TOKEN_HEREwith your AS_TOKEN
While encryption is disabled, you can add MSC4190 flags to registration files for future compatibility:
For each bridges/*/config/registration.yaml, ensure these lines exist:
de.sorunome.msc2409.push_ephemeral: true
receive_ephemeral: true
io.element.msc4190: true # For future encryption support# Restart Synapse to load double puppet appservice
docker restart matrix-synapse
sleep 15
# Restart all bridges
docker restart matrix-bridge-whatsapp matrix-bridge-signal matrix-bridge-telegram
sleep 10To ensure bridges create new unencrypted rooms with double puppet:
# Clear WhatsApp portals
docker exec matrix-postgres psql -U synapse -d whatsapp -c "DELETE FROM portal;"
docker restart matrix-bridge-whatsapp
# Clear Signal portals (if needed)
docker exec matrix-postgres psql -U synapse -d signal -c "DELETE FROM portal;"
docker restart matrix-bridge-signal
# Clear Telegram portals (if needed)
docker exec matrix-postgres psql -U synapse -d telegram -c "DELETE FROM portal;"
docker restart matrix-bridge-telegramNote: This will cause bridges to create new Matrix rooms for existing chats. Old rooms will remain but won't receive new messages.
Working:
- Bridge connected to WhatsApp/Signal/Telegram
- Double puppet configured (better message attribution)
- Messages work in unencrypted Matrix rooms (both directions)
- Messages appear from your actual user, not bot
Not working (known issue):
- Encrypted Matrix rooms — Synapse NotImplementedError with MAS + MSC4190
Bridge encryption via MSC4190 requires appservice login support in MAS, which is not yet implemented. Track progress at element-hq/matrix-authentication-service#3206.
When it lands, update bridge configs to re-enable encryption (fields vary by bridge type — check the bridge's generated config.yaml for the correct structure) and restart the bridge containers.
Bridge logs show "double puppet not enabled":
- Verify AS_TOKEN matches in both
appservices/doublepuppet.yamland bridge config - Ensure Synapse loaded the appservice (check Synapse logs for "Registered application service")
- Verify appservices directory is mounted in Synapse container
Messages still come from bot instead of my user:
- Double puppet may not be enabled yet
- Try sending
login-matrixcommand to the bridge bot - Check bridge logs for double puppet status
Encryption errors in logs:
- Ensure
encryption.allow: falsein all bridge configs - Clear portal database and restart bridges to force room recreation
Mautrix bridges were designed for manual setup with these assumptions:
- Admin manually creates and edits config files
- Admin runs bridge once to generate registration
- Admin manually copies registration to Synapse
- Admin manually configures Synapse to load registration
- Admin restarts both services
Our automated approach must handle all these steps programmatically, which creates the chicken-and-egg dependency problem.
To make this fully automatic in deploy.sh:
- Start bridges WITHOUT docker-compose dependencies
- Use docker-compose
restart: "no"for initial generation - Wait for registration files (with timeout)
- Configure Synapse dynamically
- Switch bridges to
restart: unless-stopped - Final restart of all services
Discord uses the current Go megabridge (bridgev2 framework). It is always available in the stack — no profile flag needed.
setup-bridges.sh configures Discord automatically alongside the other bridges. After it runs:
- Message
@discordbot:yourdomain.comin Matrix - Send
loginto start the OAuth2 login flow - The bot will reply with a Discord authorization URL — open it and authorize
- Once connected, the bot will bridge your Discord servers/DMs automatically
Key fields to verify after setup-bridges.sh:
homeserver:
address: http://synapse:8008
domain: yourdomain.com
appservice:
address: http://mautrix-discord:29334
database: postgres://synapse:PASSWORD@postgres/discord?sslmode=disable
bridge:
permissions:
"yourdomain.com": admin
encryption:
allow: false # required — bridge encryption not compatible with MAS
default: falsefailed to connect to homeserver— verifyappservice.addressuses the container name, not127.0.0.1- Login link times out — Discord OAuth tokens expire quickly; request a new login link
- Channels not bridging — the bridge only joins servers you authorize; send
serversto the bot to see joined servers
Slack uses the Go megabridge rewrite. It is always available — no profile flag needed.
setup-bridges.sh configures Slack automatically. After it runs:
- Message
@slackbot:yourdomain.comin Matrix - Send
login— the bot replies with a Slack App installation URL - Install the Slack app to your workspace to get the token
- Alternatively, send
login-token XOXP-...with a user token from api.slack.com/apps
homeserver:
address: http://synapse:8008
domain: yourdomain.com
appservice:
address: http://mautrix-slack:29335
database: postgres://synapse:PASSWORD@postgres/slack?sslmode=disable
bridge:
permissions:
"yourdomain.com": admin
encryption:
allow: false
default: falseas_token was not accepted— registration file not loaded; checkhomeserver.yamlincludes/appservices/slack.yaml- DMs not bridging — DM bridging requires the Slack bot to be in the workspace; use
login-tokenwith a user token for full access - Missing channels — only channels the bot is invited to are bridged; use
synccommand to refresh
Hookshot is an appservice bridge that connects Matrix rooms to external services. Enable it with --profile hookshot during deploy.sh setup.
- Generic webhooks — receive any HTTP POST and post it to a Matrix room (enabled by default)
- GitHub — issues, PRs, CI status, releases (requires OAuth app)
- GitLab — merge requests, pipelines, issues (requires access token)
- JIRA — issues, comments (requires JIRA OAuth app)
- RSS/Atom feeds — poll any feed and post updates to a room
The hookshot/config.yaml template is generated with generic webhooks enabled and GitHub/GitLab/JIRA commented out. Edit it to enable integrations:
# hookshot/config.yaml — add your credentials
github:
auth:
id: YOUR_GITHUB_APP_ID
privateKeyFile: /data/github-key.pem
oauth:
client_id: YOUR_CLIENT_ID
client_secret: YOUR_CLIENT_SECRET
redirect_uri: https://hooks.yourdomain.com/oauth/
gitlab:
instances:
gitlab.com:
url: https://gitlab.comAfter editing, restart hookshot:
docker compose --profile hookshot restart hookshot
docker compose --profile hookshot logs --tail=30 hookshot- In a Matrix room, invite
@hookshot:yourdomain.com - Send
!hookshot webhook— hookshot replies with a unique webhook URL - POST JSON to that URL from any service (GitHub Actions, CI scripts, monitoring alerts, etc.)
curl -X POST https://hooks.yourdomain.com/webhook/YOUR_TOKEN \
-H "Content-Type: application/json" \
-d '{"text": "Deployment complete!"}'Unrecognised token— theas_tokeninhookshot/registration.yamldoesn't matchappservices/hookshot.yaml; re-rundeploy.shor regenerate manuallyConnection refusedon webhook URL — verifyhookshotprofile is active and the Caddyfile has ahooks.yourdomain.comblock- GitHub webhooks not firing — check that the GitHub App webhook URL points to
https://hooks.yourdomain.com/and is active
This requires more sophisticated orchestration than docker-compose dependencies provide.