There are so many different chat platforms out there, all needing their own apps and account to use. Each one is just another app to install, and the list isn’t growing shorter. Apps like Beeper and Sunbird do already exist, but what’s the fun in that? So, I thought, let’s see if we can self host our own version of Beeper. Considering all of the core components are open source, it shouldn’t be too tricky.

There are a few main components we need in order for this to work.

  • A backend DB - I’m using PostgreSQL
  • A homeserver - Synapse
  • An authentication platform - Matrix Authentication Service
  • Bridges - e.g. Mautrix-Whatsapp
  • An admin panel (optional) - such as Element Admin

In my current setup, I am already using Traefik and Authentik, so you may need to tweak things a bit so it works with your setup.

compose.yml

This compose file has become a bit of a behemoth to behold, but comments always help.

  1services:
  2  # -------------------------
  3  # CORE: Synapse Homeserver
  4  # -------------------------
  5  main:
  6    image: matrixdotorg/synapse:latest
  7    container_name: synapse
  8    restart: unless-stopped
  9    healthcheck:
 10      test: ["CMD-SHELL", "curl -f http://localhost:8008/health || exit 1"]
 11      interval: 30s
 12      timeout: 20s      # High timeout because the Pi CPU is at 98%
 13      retries: 10       # Allow up to 5 minutes of "busy" time before marking down
 14      start_period: 3m  # Give the Pi 3 minutes to load the DB before checking
 15    environment:
 16      - SYNAPSE_SERVER_NAME=example.com
 17      - SYNAPSE_REPORT_STATS=no
 18      - PYTHONPATH=/data/py
 19      - LD_PRELOAD=/usr/lib/aarch64-linux-gnu/libjemalloc.so.2
 20    volumes:
 21      - /lab/cluster/home-1/matrix/synapse:/data
 22    networks:
 23      - matrix-net
 24      - proxy
 25      - postgres
 26      - redis
 27    deploy:
 28      resources:
 29        limits:
 30          memory: 4G
 31    labels:
 32      - traefik.enable=true
 33      - traefik.http.routers.synapse-main.entrypoints=websecure
 34      - traefik.http.routers.synapse-main.rule=Host(`matrix.example.com`) || Host(`federation.example.com`)
 35      - traefik.http.routers.synapse-main.tls=true
 36      - traefik.http.routers.synapse-main.tls.certresolver=dns-cloudflare
 37      - traefik.http.routers.synapse-main.middlewares=chain-synapse@file
 38      - traefik.http.routers.synapse-main.service=synapse-main
 39      - traefik.http.services.synapse-main.loadbalancer.server.port=8008
 40
 41  # -------------------------
 42  # AUTH: Matrix Authenticaton Server
 43  # -------------------------
 44  matrix-authentication-service:
 45    image: ghcr.io/element-hq/matrix-authentication-service:latest
 46    container_name: matrix-authentication-service
 47    restart: unless-stopped
 48    volumes:
 49      - /lab/cluster/home-1/matrix/mas/config.yaml:/app/config.yaml
 50      - /lab/cluster/home-1/matrix/mas/policy.rego:/app/policy.rego
 51    environment:
 52      - MAS_CONFIG=/app/config.yaml
 53    networks:
 54      - matrix-net
 55      - proxy
 56      - postgres
 57    deploy:
 58      resources:
 59        limits:
 60          memory: 400M 
 61    labels:
 62      - autoheal=true
 63      - traefik.enable=true
 64      - traefik.http.routers.synapse-mas-rtr.entrypoints=websecure
 65      - traefik.http.routers.synapse-mas-rtr.priority=100
 66      - "traefik.http.routers.synapse-mas-rtr.rule=((Host(`matrix.example.com`) && (PathRegexp(`^/_matrix/client/.+/(login|logout|refresh)`) || PathPrefix(`/assets`) || PathPrefix (`/auth`) || PathPrefix(`/.well-known/openid-configuration`) || PathPrefix(`/.well-known/oauth-authorization-server`))))"
 67      - traefik.http.routers.synapse-mas-rtr.tls=true
 68      - traefik.http.routers.synapse-mas-rtr.tls.certresolver=dns-cloudflare
 69      - "traefik.http.middlewares.mas-strip.stripprefix.prefixes=/auth/"
 70      - traefik.http.routers.synapse-mas-rtr.middlewares=chain-synapse@file,mas-strip
 71      - traefik.http.routers.synapse-mas-rtr.service=synapse-mas-svc
 72      - traefik.http.services.synapse-mas-svc.loadbalancer.server.port=8080
 73
 74  # -------------------------
 75  # ADINISTRATION: Element Admin
 76  # -------------------------
 77  element-admin:
 78    image: oci.element.io/element-admin:latest
 79    container_name: element-admin
 80    restart: unless-stopped
 81    environment:
 82      - SERVER_NAME=example.com
 83    networks:
 84      - proxy
 85    labels:
 86      - traefik.enable=true
 87      - traefik.http.routers.synapse-admin-rtr.entrypoints=websecure
 88      - "traefik.http.routers.synapse-admin-rtr.rule=Host(`admin.matrix.example.com`) || Host(`matrix-admin.example.com`)"
 89      - traefik.http.routers.synapse-admin-rtr.tls=true
 90      - traefik.http.routers.synapse-admin-rtr.tls.certresolver=dns-cloudflare
 91      - traefik.http.routers.synapse-admin-rtr.middlewares=chain-no-auth@file
 92      - traefik.http.routers.synapse-admin-rtr.service=synapse-admin-svc
 93      - traefik.http.services.synapse-admin-svc.loadbalancer.server.port=8080
 94
 95  # -------------------------
 96  # BRIDGE: WhatsApp
 97  # -------------------------
 98  mautrix-whatsapp:
 99    image: dock.mau.dev/mautrix/whatsapp:latest
100    container_name: mautrix-whatsapp
101    restart: unless-stopped
102    volumes:
103      - /lab/cluster/home-1/matrix/bridges/whatsapp:/data
104    networks:
105      - matrix-net
106      - postgres
107      - proxy
108    deploy:
109      resources:
110        limits:
111          memory: 600M 
112    labels:
113      - traefik.enable=true
114      - traefik.http.routers.bridge-wa.entrypoints=websecure
115      - traefik.http.routers.bridge-wa.rule=Host(`bridges.example.com`) && PathPrefix(`/v1/bridges/whatsapp`)
116      - traefik.http.routers.bridge-wa.tls=true
117      - traefik.http.routers.bridge-wa.tls.certresolver=dns-cloudflare
118      - traefik.http.middlewares.wa-strip.stripprefix.prefixes=/v1/bridges/whatsapp
119      - traefik.http.routers.bridge-wa.middlewares=wa-strip,chain-synapse@file
120      - traefik.http.routers.bridge-wa.service=wa-svc
121      - traefik.http.services.wa-svc.loadbalancer.server.port=29318
122
123  # -------------------------
124  # BRIDGE: Instagram (Meta)
125  # -------------------------
126  mautrix-meta:
127    image: dock.mau.dev/mautrix/meta:latest
128    container_name: mautrix-meta
129    restart: unless-stopped
130    volumes:
131      - /lab/cluster/home-1/matrix/bridges/meta:/data
132    networks:
133      - matrix-net
134      - postgres
135      - proxy
136    deploy:
137      resources:
138        limits:
139          memory: 600M 
140    labels:
141      - traefik.enable=true
142      - traefik.http.routers.bridge-meta.entrypoints=websecure
143      - traefik.http.routers.bridge-meta.rule=Host(`bridges.example.com`) && PathPrefix(`/v1/bridges/meta`)
144      - traefik.http.routers.bridge-meta.tls=true
145      - traefik.http.routers.bridge-meta.tls.certresolver=dns-cloudflare
146      - traefik.http.middlewares.meta-strip.stripprefix.prefixes=/v1/bridges/meta
147      - traefik.http.routers.bridge-meta.middlewares=meta-strip,chain-synapse@file
148      - traefik.http.routers.bridge-meta.service=meta-svc
149      - traefik.http.services.meta-svc.loadbalancer.server.port=29319
150
151  # -------------------------
152  # BRIDGE: Discord
153  # -------------------------
154  mautrix-discord:
155    image: dock.mau.dev/mautrix/discord:v0.7.6
156    container_name: mautrix-discord
157    restart: unless-stopped
158    volumes:
159      - /lab/cluster/home-1/matrix/bridges/discord:/data
160    networks:
161      - matrix-net
162      - postgres
163      - proxy
164    deploy:
165      resources:
166        limits:
167          memory: 600M 
168    labels:
169      - traefik.enable=true
170      - traefik.http.routers.bridge-discord.entrypoints=websecure
171      - traefik.http.routers.bridge-discord.rule=Host(`bridges.example.com`) && PathPrefix(`/v1/bridges/discord`)
172      - traefik.http.routers.bridge-discord.tls=true
173      - traefik.http.routers.bridge-discord.tls.certresolver=dns-cloudflare
174      - traefik.http.middlewares.discord-strip.stripprefix.prefixes=/v1/bridges/discord
175      - traefik.http.routers.bridge-discord.middlewares=discord-strip,chain-synapse@file
176      - traefik.http.routers.bridge-discord.service=discord-svc
177      - traefik.http.services.discord-svc.loadbalancer.server.port=29334
178
179  # -------------------------
180  # BRIDGE: Google Chat
181  # -------------------------
182  mautrix-googlechat:
183    image: dock.mau.dev/mautrix/googlechat:latest
184    container_name: mautrix-googlechat
185    restart: unless-stopped
186    volumes:
187      - /lab/cluster/home-1/matrix/bridges/googlechat:/data
188    networks:
189      - matrix-net
190      - postgres
191      - proxy
192    deploy:
193      resources:
194        limits:
195          memory: 400M
196    labels:
197      - traefik.enable=true
198      - traefik.http.routers.bridge-gchat.entrypoints=websecure
199      - traefik.http.routers.bridge-gchat.rule=Host(`bridges.example.com`) && PathPrefix(`/v1/bridges/googlechat`)
200      - traefik.http.routers.bridge-gchat.tls=true
201      - traefik.http.routers.bridge-gchat.tls.certresolver=dns-cloudflare
202      - traefik.http.middlewares.gchat-strip.stripprefix.prefixes=/v1/bridges/googlechat
203      - traefik.http.routers.bridge-gchat.middlewares=gchat-strip,chain-synapse@file
204      - traefik.http.routers.bridge-gchat.service=gchat-svc
205      - traefik.http.services.gchat-svc.loadbalancer.server.port=29320
206
207  # -------------------------
208  # BRIDGE: Google Messages
209  # -------------------------
210  mautrix-gmessages:
211    image: dock.mau.dev/mautrix/gmessages:latest
212    container_name: mautrix-gmessages
213    restart: unless-stopped
214    volumes:
215      - /lab/cluster/home-1/matrix/bridges/gmessages:/data
216    networks:
217      - matrix-net
218      - postgres
219      - proxy
220    deploy:
221      resources:
222        limits:
223          memory: 400M 
224    labels:
225      - traefik.enable=true
226      - traefik.http.routers.bridge-gmsg.entrypoints=websecure
227      - traefik.http.routers.bridge-gmsg.rule=Host(`bridges.example.com`) && PathPrefix(`/v1/bridges/gmessages`)
228      - traefik.http.routers.bridge-gmsg.tls=true
229      - traefik.http.routers.bridge-gmsg.tls.certresolver=dns-cloudflare
230      - traefik.http.middlewares.gmsg-strip.stripprefix.prefixes=/v1/bridges/gmessages
231      - traefik.http.routers.bridge-gmsg.middlewares=gmsg-strip,chain-synapse@file
232      - traefik.http.routers.bridge-gmsg.service=gmsg-svc
233      - traefik.http.services.gmsg-svc.loadbalancer.server.port=29336
234
235  # -------------------------
236  # BRIDGE: Twitter
237  # -------------------------
238  mautrix-twitter:
239    image: dock.mau.dev/mautrix/twitter:latest
240    container_name: mautrix-twitter
241    restart: unless-stopped
242    volumes:
243      - /lab/cluster/home-1/matrix/bridges/twitter:/data
244    networks:
245      - matrix-net
246      - postgres
247      - proxy
248    deploy:
249      resources:
250        limits:
251          memory: 400M 
252    labels:
253      - traefik.enable=true
254      - traefik.http.routers.bridge-twit.entrypoints=websecure
255      - traefik.http.routers.bridge-twit.rule=Host(`bridges.example.com`) && PathPrefix(`/v1/bridges/twitter`)
256      - traefik.http.routers.bridge-twit.tls=true
257      - traefik.http.routers.bridge-twit.tls.certresolver=dns-cloudflare
258      - traefik.http.middlewares.twit-strip.stripprefix.prefixes=/v1/bridges/twitter
259      - traefik.http.routers.bridge-twit.middlewares=twit-strip,chain-synapse@file
260      - traefik.http.routers.bridge-twit.service=twit-svc
261      - traefik.http.services.twit-svc.loadbalancer.server.port=29327
262
263  # -------------------------
264  # BRIDGE: LinkedIn
265  # -------------------------
266  mautrix-linkedin:
267    image: dock.mau.dev/mautrix/linkedin:latest
268    container_name: mautrix-linkedin
269    restart: unless-stopped
270    volumes:
271      - /lab/cluster/home-1/matrix/bridges/linkedin:/data
272    networks:
273      - matrix-net
274      - postgres
275      - proxy
276    deploy:
277      resources:
278        limits:
279          memory: 400M 
280    labels:
281      - traefik.enable=true
282      - traefik.http.routers.bridge-li.entrypoints=websecure
283      - traefik.http.routers.bridge-li.rule=Host(`bridges.example.com`) && PathPrefix(`/v1/bridges/linkedin`)
284      - traefik.http.routers.bridge-li.tls=true
285      - traefik.http.routers.bridge-li.tls.certresolver=dns-cloudflare
286      - traefik.http.middlewares.li-strip.stripprefix.prefixes=/v1/bridges/linkedin
287      - traefik.http.routers.bridge-li.middlewares=li-strip,chain-synapse@file
288      - traefik.http.routers.bridge-li.service=li-svc
289      - traefik.http.services.li-svc.loadbalancer.server.port=29341
290
291  draupnir:
292    container_name: draupnir
293    volumes:
294      - /lab/cluster/home-1/matrix/draupnir:/data
295    image: gnuxie/draupnir:latest
296    command: bot --draupnir-config /data/config/production.yaml
297    networks:
298      - matrix-net
299      - postgres
300
301networks:
302  matrix-net:
303  proxy:
304    external: true
305  postgres:
306    external: true
307  redis:
308    external: true

Traefik Config

Synapse and web-based clients (like my Finny) are incredibly picky about CORS and security headers. If you don’t get these right, your client will just hang on “Connecting” forever. I use a file-based provider in Traefik to keep things clean.

/clstr/traefik/rules/chain-synapse.yml

1http:
2  middlewares:
3    chain-synapse:
4      chain:
5        middlewares:
6          - synapse-cors
7          - middlewares-rate-limit
8          - middlewares-secure-headers

/clstr/traefik/rules/synapse-cors.yml

 1http:
 2  middlewares:
 3    synapse-cors:
 4      headers:
 5        accessControlAllowOriginList:
 6          - "*"
 7        accessControlAllowMethods:
 8          - "GET", "POST", "PUT", "DELETE", "OPTIONS"
 9        accessControlAllowHeaders:
10          - "*"

Initial configuration

My setup follows the standard monoloth setup. This is because, when running on a Pi5, the extra RAM reuiqred for each worker, outweighs the performance gains.

/clstr/matrix/synapse/homeserver.yaml

  1# ----------------------------------------------------------------------
  2# SYNAPSE CONFIGURATION FOR EXAMPLE.COM
  3# ----------------------------------------------------------------------
  4
  5# The domain part of your user IDs (e.g. @user:example.com)
  6server_name: "example.com"
  7
  8# The public URL where clients/servers connect (Reverse Proxy URL)
  9public_baseurl: "https://matrix.example.com"
 10
 11# Location of the PID file
 12pid_file: /data/homeserver.pid
 13
 14suppress_key_server_warning: true
 15
 16# ----------------------------------------------------------------------
 17# LISTENERS
 18# ----------------------------------------------------------------------
 19listeners:
 20  - port: 8008
 21    tls: false
 22    type: http
 23    x_forwarded: true # Trusts Nginx/Traefik headers
 24    bind_addresses: ['::', '0.0.0.0']
 25    resources:
 26      - names: [client,openid,static,federation,media]
 27        compress: false
 28  - port: 9093
 29    bind_address: '0.0.0.0'
 30    type: http
 31    resources:
 32     - names: [replication]
 33
 34# ----------------------------------------------------------------------
 35# DATABASE (PostgreSQL)
 36# ----------------------------------------------------------------------
 37database:
 38  name: psycopg2
 39  args:
 40    user: synapse
 41    password: "REDACTED"
 42    database: synapse
 43    host: pgbouncer
 44    cp_min: 2
 45    cp_max: 5
 46
 47# ----------------------------------------------------------------------
 48# AUTHENTICATION & LDAP
 49# ----------------------------------------------------------------------
 50
 51matrix_authentication_service:
 52  enabled: true
 53  endpoint: http://matrix-authentication-service:8080/
 54  secret: "Nbal2MLkHK2wfd3WvHnqk0d4C1OhR56X"
 55
 56# Disable open registration
 57enable_registration: false
 58
 59experimental_features:
 60  # Enable MSC4108 (QR code login)
 61  msc4108_enabled: true
 62
 63# ----------------------------------------------------------------------
 64# LOGGING & STATS
 65# ----------------------------------------------------------------------
 66report_stats: false
 67
 68# ----------------------------------------------------------------------
 69# SECRETS (Preserved from your generated file)
 70# ----------------------------------------------------------------------
 71registration_shared_secret: "REDACTED"
 72macaroon_secret_key: "REDACTED"
 73form_secret: "REDACTED"
 74signing_key_path: "/data/vmd1.dev.signing.key"
 75
 76# ----------------------------------------------------------------------
 77# FEDERATION
 78# ----------------------------------------------------------------------
 79trusted_key_servers:
 80  - server_name: "matrix.org"
 81
 82# ----------------------------------------------------------------------
 83# MEDIA
 84# ----------------------------------------------------------------------
 85media_store_path: /data/media_store
 86max_upload_size: 104857600 # 100MB Limit
 87url_preview_enabled: true
 88url_preview_ip_range_blacklist:
 89  - '127.0.0.0/8'
 90  - '10.0.0.0/8'
 91  - '172.16.0.0/12'
 92  - '192.168.0.0/16'
 93  - '100.64.0.0/10'
 94  - '192.0.0.0/24'
 95  - '169.254.0.0/16'
 96  - '192.88.99.0/24'
 97  - '198.18.0.0/15'
 98  - '198.51.100.0/24'
 99  - '203.0.113.0/24'
100  - '224.0.0.0/4'
101  - '::1/128'
102  - 'fe80::/10'
103  - 'fc00::/7'
104  - '2001:db8::/32'
105  - 'ff00::/8'
106  - 'fec0::/10'
107
108# ----------------------------------------------------------------------
109# EMAIL
110# ----------------------------------------------------------------------
111
112email:
113  # The hostname of the outgoing SMTP server
114  smtp_host: "mail-eu.smtp2go.com"
115
116  # The port. Common ports: 587 (STARTTLS), 465 (Implicit TLS), or 25 (Plain)
117  smtp_port: 587
118
119  # Username and password for the SMTP server
120  smtp_user: "REDACTED"
121  smtp_pass: "REDACTED"
122
123  # TLS/SSL Settings (See explanation below)
124  # Use force_tls: true for port 465. Use require_transport_security: true for port 587.
125  force_tls: false
126  require_transport_security: true
127
128  # The "From" address for emails sent by Synapse
129  notif_from: "Synapse <REDACTED>"
130
131  # Enable email notifications (missed messages, mentions, etc.)
132  enable_notifs: true
133
134  # Automatically subscribe new users to email notifications
135  notif_for_new_users: true
136
137  # Custom URL for client links (e.g., in password reset emails)
138  # Set this to your Element/web client URL.
139  client_base_url: "https://chat.example.com"
140
141  # (Optional) Lifetime of the validation token
142  validation_token_lifetime: 1h
143
144# ----------------------------------------------------------------------
145# BRIDGES (APP SERVICES)
146# ----------------------------------------------------------------------
147# UNCOMMENT these lines only AFTER you have generated the registration.yaml
148# for the specific bridge and copied it into the synapse data folder.
149# If you leave these uncommented but the files don't exist, Synapse will crash.
150
151app_service_config_files:
152  - /data/bridge-registrations/whatsapp-registration.yaml
153  - /data/bridge-registrations/discord-registration.yaml
154  - /data/bridge-registrations/meta-registration.yaml
155  - /data/bridge-registrations/googlechat-registration.yaml
156  - /data/double-puppet/doublepuppet.yaml
157  - /data/bridge-registrations/gmessages-registration.yaml
158  - /data/bridge-registrations/linkedin-registration.yaml
159  - /data/bridge-registrations/twitter-registration.yaml
160
161# ----------------------------------------------------------------------
162# REDIS
163# ----------------------------------------------------------------------
164redis:
165  enabled: true
166  host: redis
167
168# ----------------------------------------------------------------------
169# PERFORMANCE
170# ----------------------------------------------------------------------
171caches:
172   global_factor: 2.0
173
174presence:
175  enabled: false

You can generate the secret values by generating a config file using the Synapse docker image:

1docker run -it --rm \
2    --mount type=bind,src=$(pwd),dst=/data \
3    -e SYNAPSE_SERVER_NAME=example.com \
4    -e SYNAPSE_REPORT_STATS=yes \
5    matrixdotorg/synapse:latest generate

Matrix Authentication Service

MAS is the modern way to handle Matrix auth. In my cluster, MAS acts as an OIDC proxy that talks to Authentik. This means I have one single place to manage my users.

/clstr/matrix/mas/config.yaml

You can generate the base of this with mas-cli config generate, but here is the redacted version of my production config. Make sure the matrix.secret matches the one in your homeserver.yaml.

 1http:
 2  listeners:
 3  - name: web
 4    resources:
 5    - name: discovery
 6    - name: human
 7    - name: oauth
 8    - name: compat
 9    - name: graphql
10    - name: assets
11    - name: adminapi
12    binds:
13    - address: '[::]:8080'
14  public_base: https://matrix.example.com/auth/
15
16database:
17  uri: postgres://synapse:REDACTED@pgbouncer/matrix_authentication_service?sslmode=disable
18
19secrets:
20  encryption: REDACTED_HEX_SECRET
21  keys:
22  - key: |
23      -----BEGIN RSA PRIVATE KEY-----
24      # Generate your own keys!
25      -----END RSA PRIVATE KEY-----
26
27matrix:
28  kind: synapse
29  homeserver: example.com
30  secret: Nbal2MLkHK2wfd3WvHnqk0d4C1OhR56X
31  endpoint: http://synapse:8008/
32
33upstream_oauth2:
34  providers:
35    - id: 01HFRQFT5QFMJFGF01P7JAV2ME
36      synapse_idp_id: oidc-authentik
37      human_name: Authentik
38      issuer: "https://authentik.example.com/application/o/synapse/"
39      client_id: "REDACTED"
40      client_secret: "REDACTED"
41      scope: "openid profile email"
42      claims_imports:
43        localpart:
44          action: require
45          template: "{{ user.preferred_username }}"

For the Authentik side of things, check out the MAS documentation to set up your OAuth provider.

Bridge Config

As you can see in the syanpse’s app_service_config_files key, there are various bridges which have been connected to synapse. These config files need to be generated. I’m not going to rewrite the mautrix docs, so it’s best to just follow the documentation for each bridge when it comes to setting it up.

I would just like to draw attention to the following, however, as this is crucial to ensure optimal performance of ALL bridges:

Double-puppeting

Instead of requiring everyone to manually enable double puppeting, you can give the bridge access to enable double puppeting automatically. This makes the process much smoother for users, and removes problems like access tokens getting invalidated.

Previously there were multiple different automatic double puppeting methods, but the older methods are deprecated and were completely removed in the megabridge rewrites. Only the new appservice method is now supported.

Automatic double puppeting should work on all homeserver implementations that support appservices. However, some servers don’t follow the spec, and may not work with a null url field.

Using appservices means it requires administrator access to the homeserver, so it can’t be used if your account is on someone elses server (e.g. using self-hosted bridges from matrix.org). In such cases, manual login is the only option.

This method also makes timestamp massaging work correctly and disables ratelimiting for double puppeted messages.

  1. First create a new appservice registration file. The name doesn’t really matter, but doublepuppet.yaml is a good choice. Don’t touch the bridge’s main registration file, and make sure the ID and as/hs tokens are different (having multiple appservices with the same ID or as_token isn’t allowed).

     1
     2# The ID doesn't really matter, put whatever you want.
     3id: doublepuppet
     4# The URL is intentionally left empty (null), as the homeserver shouldn't
     5# push events anywhere for this extra appservice. If you use a
     6# non-spec-compliant server, you may need to put some fake URL here.
     7url:
     8# Generate random strings for these three fields. Only the as_token really
     9# matters, hs_token is never used because there's no url, and the default
    10# user (sender_localpart) is never used either.
    11as_token: random string
    12hs_token: random string
    13sender_localpart: random string
    14# Bridges don't like ratelimiting. This should only apply when using the
    15# as_token, normal user tokens will still be ratelimited.
    16rate_limited: false
    17namespaces:
    18  users:
    19  # Replace your\.domain with your server name (escape dots for regex)
    20  - regex: '@.*:your\.domain'
    21    # This must be false so the appservice doesn't take over all users completely.
    22    exclusive: false
    
  2. Install the new registration file the usual way (see Registering appservices).

  3. Finally set as_token:$TOKEN as the secret in double_puppet -> secrets (e.g. if you have as_token: meow in the registration, set as_token:meow in the bridge config).

    1
    2double_puppet:
    3  ...
    4  secrets:
    5    your.domain: "as_token:meow"
    6  ...
    

    N.B. For old bridges, the map is bridge -> login_shared_secret_map.

If you set up double puppeting for multiple bridges, you can safely reuse the same registration by just setting the same token in the config of each bridge (i.e. no need to create a new double puppeting registration for each bridge).

This method works for other homeservers too, you just have to create a new registration file for each server, add the token to secrets, and also add the server address to the servers map (for the bridge server, adding to the server map is not necessary as it defaults to using the one configured in homeserver -> address).

Bridge Setup

Follow the steps here to do so. All bridges are run in docker, for convenience and security.

Setup Summary

Generate config

1docker run -it --rm \
2    -v $(pwd):/data:z \
3    dock.mau.dev/mautrix/whatsapp:latest

Edit the default config file to add your database, synapse and bridge config.

Generate Registration file

1docker run -it --rm -v $(pwd):/data:z dock.mau.dev/mautrix/whatsapp:latest -g

Add to synapse in the app_service_config_files key

Prepping the database

On the assumption that you are using a PostgreSQL DB, you need to configure a user, and database for each and every one of the bridges and synapse itself. Synapse requires the Database to be in the ‘C’ locale, so see below how to do:

1createuser --pwprompt synapse
2createdb --encoding=UTF8 --locale=C --template=template0 --owner=synapse synapse

Repeat for each bridge, to create users, and their corresponding databases.

.well-known delegation

To setup federation, you need to setup .well-known delegation. To do this, serve a json file at DOMAIN/.well-known/matrix/server

This should be in the following format:

1{
2  "m.server": "federation.example.com:443"
3}

Another useful one to configure is the client delegation, which allows clients to automatically discover your matrix server.

Serve a file at DOMAIN/.well-known/matrix/client , with the following format:

1{
2  "m.homeserver": {
3    "base_url": "https://matrix.example.com"
4  }
5}

Start-up

Now, start your Synapse instance with everything configured, and boom, you have a functioning Synapse instance.

The client

To match Beeper’s aesthetic, and UI-driven bridge management, I use my own custom client called Finny, a fork of Cinny. The desktop app is a WIP, but the web app is deployed on my infrastructure.

Final Thoughts

Overall, this is a pretty cool system, allowing you to see all your chats in one place, and have your own matrix server, ensuring your privacy and control of your own data.