Deploy a Whatsapp matrix bridge on Synapse

So you have a Pinephone - a SOC with an integrated display and a radio, which runs linux, but not a smartphone (seriously, NOT a smartphone!), and you want to use Whatsapp from there.


This is the echoes of the loughs which you’ll hear, usually laughs of desperation.

Unfortunately Linux phones systems are not nearly as mature as the oldest android/windows CE phone which you used to use - but Whatsapp (or any other messenger) might come in help and give you that feel of comfort. It could really bring some light in the sad panorama of usability in the portable linux devices.

I’m pretty sure is plenty of people who is able to live with their Pinephones (see the plural), unfortunately I’m not (yet). For all of you who will feel offended by my words, I’m so happy that you are happy with your crappy phones usability. Pinephone, as a phone, is as useful as a rotary dial phone in the year 2023. Better go for a banana phone - at least it can be used as a mobile phone!

You need a Matrix Homeserver

The Matrix Homeserver is the foundation of the whole system.
There are many servers available, Synapse seemed a good and decent choice.
The Docker version is well maintained and the compose stackfile is simple enough:

version: '3'


    image: "matrixdotorg/synapse:latest"
    restart: unless-stopped
      - default
      - synapse
      - postgres
      - ./data/synapse:/data
      VIRTUAL_HOST: ""
      VIRTUAL_PORT: 8008
      - "8008:8008/tcp"

    restart: unless-stopped
      - default
      POSTGRES_USER: synapse
      POSTGRES_DB: synapse
      POSTGRES_INITDB_ARGS: --encoding=UTF-8 --lc-collate=C --lc-ctype=C
      - ./data/postgres:/var/lib/postgresql/data

    external: true

The execution of Synapse with the above stack file requires the below steps:

mkdir -p ./data/synapse
mkdir -p ./data/postgres
docker network create synapse

(this creates an external network, used by all the bridges)

Synapse and all the matrix bridges has a quite peculiar way to be configured.
Instead of providing a configuration file, the file is created at first run.
The two variables VIRTUAL_HOST and SYNAPSE_SERVER_NAME are used to initialize the config file.

To generate the synapse config file:

docker compose run --rm synapse generate

The server name is important, check the official documentation
In my case, I wanted specifically to use (for both the server and the hosting) the domain name
The matrix federation allows multiple ways to shorten the domain name and use just the secondary - although this is not your business.

The configuration file looks like the below:

# Configuration file for Synapse.
# This is a YAML file: see [1] for a quick introduction. Note in particular
# that *indentation is important*: all the elements of a list or dictionary
# should have the same indentation.
# [1]
# For more information on how to configure Synapse, including a complete accounting of
# each option, go to docs/usage/configuration/ or
server_name: ""
pid_file: /data/
  - port: 8008
    tls: false
    type: http
    x_forwarded: true
      - names: [client, federation]
        compress: false

#  name: sqlite3
#  args:
#    database: /data/homeserver.db

    name: psycopg2
        user: synapse
        password: XXX
        host: postgres
        database: synapse
        cp_min: 5
        cp_max: 10

log_config: "/data/"
media_store_path: /data/media_store
registration_shared_secret: "XXX"
report_stats: true
macaroon_secret_key: "XXX"
form_secret: "XXX"
signing_key_path: "/data/"
  - server_name: ""

You are expected to adjust the database section, eventually using postgres, like in the above example.
SQLite works well, although managing postgres in Docker is not so complex, therefore worth a try.

The users creation is disabled by default. Users can be registered enabling the self registration:

enable_registration: true
enable_registration_without_verification: true

although this should be disabled right after (to avoid being target of spammers).

You can also register users manually using the console (with the stack up):

docker exec -it synapse-synapse-1 bash
register_new_matrix_user -c homeserver.yaml http://localhost:8008

Run Synapse Homeserver

To run the server, just turn on the stack:

docker compose up # use -d to put the stack in background

The Matrix server will be up and running, listening on port 8008 of your system.

This is quite good, but to publish it over the public internet, a reverse proxy is better. For example you’ll want to deploy an nginx server and use certbot to obtain an SSL certificate.

This can be achieved quite easily, I suggest to read and

This two how-to are quite enough detailed to achieve your goal.

To check that Synapse is running, open a webpage to http://localhost:8008 or to the domain/url you used.
The Synapse welcome page should appear.

If you configured the reverse proxy according to the official documentation, only the _matrix paths gets forwarded.

Nginx on system or on container?

I prefer for such services to deploy Nginx to the operative system, and use Docker for the running services.
Eventually a new stack for Nginx can be configured.
Having Nginx deployed at OS level allows easier configuration only.

The Mautrix Whatsapp bridge

With a running Synapse server, on his dedicated Docker compose stack file, running and working (check it out on Element or any other Matrix client!) - it’s time to deploy the Whatsapp bridge.

This is a local server which simulates a browser and consumes the Web API for Whatsapp.
It requires a running Whatsapp client on a phone - but not 24/7 running (just every 2 weeks).

The stack file is similar to the Synapse one:

version: "3.7"

    container_name: mautrix-whatsapp
    restart: unless-stopped
    - ./data/whatsapp:/data

    # If you put the service above in the same docker-compose as the homeserver,
    # ignore the parts below. Otherwise, see below for configuring networking.

    # If synapse is running outside of docker, you'll need to expose the port.
    # Note that in most cases you should either run everything inside docker
    # or everything outside docker, rather than mixing docker things with
    # non-docker things.
    #- "29318:29318"
    # You'll also probably want this so the bridge can reach Synapse directly
    # using something like `http://host.docker.internal:8008` as the address:
    #- "host.docker.internal:host-gateway"

    # If synapse is in a different network, then add this container to that network.
      - synapse
      - default

    restart: unless-stopped
      - default
      POSTGRES_USER: whatsapp
      POSTGRES_DB: whatsapp
      POSTGRES_INITDB_ARGS: --encoding=UTF-8 --lc-collate=C --lc-ctype=C
      - ./data/postgres:/var/lib/postgresql/data

    external: true

The generation of the configuration file is similar to the Synapse one:

mkdir -p ./data/whatsapp
mkdir -p ./data/postgres
docker compose run --rm mautrix-whatsapp

This will generate a file in ./data/whatsapp named config.yaml.
The file is too big to show here, although there are just few lines to change:

    # The address that this appservice can use to connect to the homeserver.
    address: http://synapse:8008
    # The domain of the homeserver (also known as server_name, used for MXIDs, etc).

    # The address that the homeserver can use to connect to this appservice.
    address: http://mautrix-whatsapp:29318

    # Database config.
        # The database type. "sqlite3-fk-wal" and "postgres" are supported.
        type: postgres
        # The database URI.
        #   SQLite: A raw file path is supported, but `file:<path>?_txlock=immediate` is recommended.
        #   Postgres: Connection string. For example, postgres://user:password@host/database?sslmode=disable
        #             To connect via Unix socket, use something like postgres:///dbname?host=/var/run/postgresql
        uri: postgres://whatsapp:XXX@postgres/whatsapp?sslmode=disable
        # Maximum number of connections. Mostly relevant for Postgres.
        max_open_conns: 20
        max_idle_conns: 2
        # Maximum connection idle time and lifetime before they're closed. Disabled if null.
        # Parsed with
        max_conn_idle_time: null
        max_conn_lifetime: null

        "*": relay
        "": user
        "": admin

The configuration is done, but the bridge has still to generate a registration.yaml file, which is used by Synapse to communicate to the bridge.
This is obtained running the service once, again:

docker compose run --rm mautrix-whatsapp

Check the file in ./data/whatsapp named registration.yaml. This file must be copied to the data folder of Synapse!

cp ./data/whatsapp/registration.yaml [synapse data folder]/registration-whatsapp.yaml

Configure Synapse for Whatsapp bridge

Change the Synapse homserver confguration and add at the end of the data/synapse/homeserver.yaml file:

  - /data/registration-whatsapp.yaml

Restart the Synapse server:

docker compose down
docker compose up # -d

Start Whatsapp bridge

With all the configurations done, the bridge can now be started:

cd mautrix-whatsapp
docker compose up

The authentication requires some steps done in a Matrix client. The official documentation covers all the steps very well.

Filesystem configuration

Internet has some (not many) examples about how to run all these tools together.
I struggled a bit to understand the relation between all the components, although I ended up doing:

  • deploy a vanilla Debian server (or any other distro)
    • better to encrypt the disk
  • install Docker CE
  • create the stack files in /opt
    • synapse will have his own stack file /opt/synapse
    • whatsapp will have his own as well in /opt/mautrix-whatsapp
    • eventually telegram and Sygnal bridges can be deployed too (always on /opt/xxx)

I opted to replicate the deployment of Postgres on all the servers.

Final remarks

The system works very well and with servers federation you can disable users registration and stay with your own private server (but chat with others).
I did not cover any VoIP - could be interesting for the future.