Skip to content

Matrix and mautrix bridges

How DNF implements the Matrix server (Synapse) and mautrix bridges. For day-to-day operation, see Matrix et les ponts.

Everything is handled by dnf/modules/service/matrix.nix, with two satellite modules : element.nix (static web client) and turn.nix (coturn relay for audio/video).

Diagram
  • Caddy routes /_matrix/* and /_synapse/client/* to Synapse and serves /.well-known/matrix/{client,server} files (client discovery and federation).
  • Synapse listens on dnfConfig.network.ports.matrix, local PostgreSQL database (created with C collation by a matrix-db-init unit).
  • Kanidm provides authentication : OIDC client provisioned via darkone.service.idm.oauth2.matrix, local part derived from preferred_username (see Authentication & IDM).
  • coturn is wired automatically if the turn service is declared (turn_uris, shared secret via sops).

matrix.nix is a lib.mkMerge of independent blocks:

BlockConditionContent
BasealwaysOIDC client, reverse proxy, persistence
Servermatrix.enableSynapse, PostgreSQL, common secrets, doublepuppet appservice
One block per bridgematrix.enable && bridges.<x>.enablesops secrets + bridge’s mautrix service

Each bridge can thus be disabled individually (darkone.service.matrix.bridges.{whatsapp,signal,telegram,messenger,discord}.enable), its sops secrets only being declared if the bridge is active.

DNF relies on the nixpkgs services.mautrix-* modules, which generate the appservice registration and register it with Synapse on startup. Two generations of bridges coexist, with different configuration keys:

BridgeGenerationDouble puppetingLevel
whatsappbridgev2 (Go)double_puppet.secretsuser
signalbridgev2 (Go)double_puppet.secretsuser
messenger (meta)bridgev2 (Go)double_puppet.secretsuser
telegramlegacy (Python)bridge.login_shared_secret_mapfull
discordlegacy (Go)bridge.login_shared_secret_mapuser

The mkBridgePermissions helper (in matrix.nix) builds the same policy for all bridges:

permissions:
"domain.tld": user # tout compte local, avec son propre compte distant
"@alice:domain.tld": admin # le matrix.admin déclaré dans etc/config.yaml

Distant sessions are isolated per Matrix user: generalizing to all domain accounts carries no risk of interference.

Official appservice 🡕 method: a doublepuppet appservice is registered in Synapse (app_service_config_files) with a namespace covering @.*:domain.tld. Its as_token allows each bridge to emit events on behalf of the user’s real account.

Diagram

The token is unique for all bridges (mautrix-doublepuppet-as-token) and injected into each config by environment variable substitution (as_token:$MAUTRIX_DOUBLEPUPPET_AS_TOKEN), as nixpkgs modules pass the configuration through envsubst with the service’s environmentFile.

The registration of an appservice is the contract between Synapse and the bridge. It contains two tokens, one per authentication direction :

TokenDirectionUsage
as_tokenbridge → Synapsethe bridge authenticates on the client API (sending messages, sync…)
hs_tokenSynapse → bridgeSynapse authenticates when pushing events to the bridge (/transactions)

By default, nixpkgs modules generate these tokens randomly on first start and store them in /var/lib/mautrix-*. Consequence : any reset of the state directory changes the token pair, while Synapse — which reads registrations only at startup — keeps the old one in memory ; the bridge is then rejected (« The as_token was not accepted »).

DNF therefore fixes both tokens of each bridge via sops : the registration becomes a pure function of secrets. Resetting a bridge’s state no longer invalidates anything on Synapse’s side, and the whole setup is reproducible (redeployment, migration, backup restoration).

  • Secrets : declared by block in matrix.nix (sops.secrets.*), and assembled into environment files by sops.templates.* owned by the bridge’s system user. Full list in the operations page.
  • Ports : matrix, matrixTelegram and matrixDiscord are set by the dnf/config/network.nix registry ; the default appservice ports of other bridges (29318, 29319, 29328) are listed there as reserved to prevent any future collision.
  • WhatsApp statuses : network.enable_status_broadcast = false in the bridge config, otherwise the bridge endlessly recreates the “WhatsApp Status Broadcast” room for each user.