Rationale: Synapse runs as Docker on Contabo (ref: `synapse.md` — "Docker Compose at /opt/matrix/docker-compose.yml"). The bridge container joins the existing `matrix-net` network so it can reach both `matrix-synapse` and `matrix-postgres` by container name without exposing any new ports externally.
- **Released:** 2026-03-16 (latest stable as of 2026-04-09)
- **Type:** Go rewrite (NOT the deprecated Python mautrix-signal)
Pin to the exact tag `v0.2603.0`, not `:latest`, so upgrades are intentional.
## 3. Database Plan
Create a new Postgres database and role inside the existing `matrix-postgres` container. The role gets **minimal grants** — `LOGIN` only, no `SUPERUSER`, no `CREATEDB`, no `CREATEROLE`. Ownership of the new database is the sole privilege.
```sql
-- Connect as the synapse superuser to create the role and database
CREATE ROLE mautrix_signal WITH LOGIN PASSWORD '<generated-64-char-password>'
NOSUPERUSER NOCREATEDB NOCREATEROLE;
CREATE DATABASE mautrix_signal
OWNER mautrix_signal
ENCODING 'UTF8'
LC_COLLATE 'C'
LC_CTYPE 'C';
-- No additional GRANT needed — OWNER on the database gives full DDL/DML
-- within mautrix_signal only. The role has zero access to synapse or mas databases.
```
Verification after creation:
```sql
-- Confirm no superuser, no createdb
SELECT rolname, rolsuper, rolcreatedb, rolcreaterole FROM pg_roles WHERE rolname = 'mautrix_signal';
- **No Caddy changes needed** — bridge communicates with Synapse over the internal Docker network
- **No firewall changes needed** — no host port mapping
The bridge container joins `matrix-net` in docker-compose.yml:
```yaml
mautrix-signal:
image: dock.mau.dev/mautrix/signal:v0.2603.0
container_name: mautrix-signal
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
volumes:
- ./mautrix-signal:/data
networks:
- matrix-net
```
No `ports:` section — the container is only reachable from within `matrix-net`.
## 5. Encryption Config
Enable end-to-bridge encryption with MSC4190 (required for MAS compatibility).
### 5a. Bridge config (`config.yaml`)
```yaml
encryption:
allow: true
default: true
require: true
appservice: false
msc4190: true # REQUIRED when MAS is in use
allow_key_sharing: true
pickle_key: generate
self_sign: true
```
**MSC4190 requirement:** The mautrix docs state: "The `encryption` -> `msc4190` config option must be set to true for encryption to work if you use MAS."
The current `homeserver.yaml` has no `experimental_features` block. The exact diff:
```diff
--- a/homeserver.yaml
+++ b/homeserver.yaml
@@ -end of file
+
+experimental_features:
+ msc3202_transaction_extensions: true
+ msc2409_to_device_messages_enabled: true
```
These flags enable:
-`msc3202_transaction_extensions`: Allows Synapse to send to-device messages, device list changes, and OTK counts in appservice transaction pushes (required for E2BE)
-`msc2409_to_device_messages_enabled`: Allows appservices to receive to-device messages (required for encryption key exchange)
**Note:** MSC4190 is NOT an `experimental_features` flag. It is parsed from the appservice registration YAML file as `io.element.msc4190: true` (confirmed from Synapse 1.147.1 source: `synapse/config/appservice.py` line 190: `msc4190_enabled = as_info.get("io.element.msc4190", False)`).
The bridge process holds plaintext message content in memory after decryption. The trust boundary extends from the Matrix client to the bridge container. Signal transport encryption remains intact on the Signal side.
## 6. MAS Interaction
### Claim: Appservice registration bypasses MAS
**Status: ASSUMPTION — verify during Phase 3 with stop-and-check.**
The evidence supporting this claim:
1. Appservices authenticate to Synapse via `as_token`/`hs_token` in the registration YAML, which is a Synapse-native mechanism predating MAS.
2. GitHub issue element-hq/matrix-authentication-service#3206 shows a user successfully registering and running a mautrix-signal appservice alongside MAS — basic appservice connectivity (GET `/versions`, GET `/account/whoami`, appservice ping) all returned 200 before encryption was attempted.
3. A commenter on that issue confirmed: "Can confirm it works with mautrix-signal and mautrix-whatsapp."
4. The Matrix Application Service spec defines its own auth mechanism independent of the C-S API auth layer.
**However**, no official MAS documentation explicitly states "appservices bypass MAS." The MAS docs (element-hq.github.io/matrix-authentication-service/) have no dedicated appservice compatibility page.
**Phase 3 stop-and-check procedure:**
1. After adding the registration to `homeserver.yaml` and restarting Synapse, check Synapse logs for appservice registration errors
2. Before starting the bridge container, run a manual appservice ping test:
- Response body contains `"user_id":"@signalbot:echo6.co"`
- Response body contains `"appservice_id":"signal"` (confirms Synapse recognized the as_token as appservice auth)
**FAIL criteria (ANY triggers STOP):**
- HTTP status `401` or `403` → MAS or Synapse rejected the as_token
- HTTP `3xx` redirect to MAS (`Location:` header pointing to `matrix-mas:8080` or `matrix.echo6.co` auth endpoints)
- Response contains MAS-specific indicators: HTML login page, `matrix-authentication-service` in headers/body, or `errcode: M_UNKNOWN_TOKEN` with MAS introspection trace in Synapse logs
-`user_id` in response does NOT match `@signalbot:echo6.co`
**On FAIL:** Do NOT start the bridge container. Do NOT proceed to step 16. Capture the full `curl -sv` output (headers + body) and the last 50 lines of Synapse logs (`docker logs matrix-synapse --tail 50`). Report both for triage.
### Registration steps
1. Generate the registration file by running the bridge container once with config in place
2. Copy `registration.yaml` into the Synapse data volume (`/opt/matrix/synapse/`)
3. Add to `homeserver.yaml`:
```yaml
app_service_config_files:
- /data/registration.yaml
- /data/doublepuppet.yaml
```
Both files are listed — Synapse reads all entries on startup. A single restart covers both registrations.
4. Restart Synapse (`docker compose restart synapse`) to pick up both appservice registrations
## 7. Permissions
```yaml
bridge:
permissions:
"*": relay
"echo6.co": user
"@matt:echo6.co": admin
```
-`*: relay` — external users can interact via relay (relay disabled by default, so effectively no access)
-`echo6.co: user` — all echo6.co users can use the bridge
-`@matt:echo6.co: admin` — full admin access for matt
Single-user deployment — only matt will link a Signal account.
## 8. Double Puppeting
Use the **appservice-based automatic double puppeting** method:
1. Generate a dedicated double-puppet appservice registration (`doublepuppet.yaml`) with a null URL and an `as_token`:
```yaml
id: doublepuppet
url:
as_token: <generated-token>
hs_token: <generated-token>
sender_localpart: _doublepuppet
rate_limited: false
namespaces:
users:
- regex: '@.*:echo6\.co'
exclusive: false
```
2. Register it with Synapse alongside the bridge registration in `app_service_config_files` (see section 6)
This ensures messages matt sends from Signal Desktop appear as `@matt:echo6.co` in Matrix rooms rather than as the Signal ghost user.
**MAS compatibility:** The appservice-based double puppeting method uses the appservice `as_token` to impersonate the user, which works independently of MAS. MAS handles human user auth; appservice tokens are Synapse-native. (Same assumption as section 6 — covered by the stop-and-check.)
The existing backup script (`/opt/matrix/scripts/pg_backup.sh`) only backs up the `synapse` database (ref: `synapse.md` — "Backs up synapse DB only (NOT mas DB)").