Rust Matrix E2EE relay with durable delivery and rollout qualification
Find a file
2026-08-14 21:33:42 +02:00
src chore(relay): remove mention diagnostics 2026-08-14 21:33:42 +02:00
tests fix(relay): decode formatted mentions 2026-08-14 21:15:40 +02:00
.gitignore docs: publish standalone relay source 2026-08-12 22:38:19 +02:00
.gitleaksignore chore: allowlist Matrix public-key fixture 2026-08-12 22:42:56 +02:00
Cargo.lock fix(relay): decode formatted mentions 2026-08-14 21:15:40 +02:00
Cargo.toml fix(relay): decode formatted mentions 2026-08-14 21:15:40 +02:00
LICENSE docs: publish standalone relay source 2026-08-12 22:38:19 +02:00
README.md fix(runtime): support joined unencrypted rooms 2026-08-14 13:58:16 +02:00

matrix-relay-bot

Rust Matrix E2EE relay with two integrations:

Matrix mention -> authenticated webhook JSON
Authenticated POST /send -> Matrix text or media event

The consuming environment owns deployment, credentials, and runtime state. The bot logs into an existing Matrix account once to create its own device and crypto store. Normal service startup restores that session; it never receives the Matrix password.

Requirements

  • Rust 1.93 or newer
  • Cargo
  • OpenSSL and pkg-config
  • An existing Matrix account joined to the required encrypted rooms

Build and test

cargo build --locked
cargo test --all-features
cargo clippy --all-targets --all-features -- -D warnings

First initialization

Store the Matrix password in a mode-private file, then run:

matrix-relay-bot init \
  --matrix-homeserver-url https://matrix.example.test \
  --matrix-bot-user-id @relay:example.test \
  --matrix-device-id N8NRELAY02 \
  --matrix-device-display-name 'n8n Matrix Relay (Rust)' \
  --matrix-storage-dir /var/lib/matrix-relay-bot/rust \
  --matrix-password-file /run/credentials/matrix-relay-bootstrap.service/password

init performs exactly one password login for the fixed device ID, creates the Matrix SDK SQLite crypto store, performs one initial sync, and atomically commits:

/var/lib/matrix-relay-bot/rust/
├── crypto/
├── outbox.sqlite
└── session.json

Running init again accepts complete matching state without another login. Partial state, identity drift, or homeserver drift fails. A failed first attempt leaves the sibling .rust.init directory for diagnosis; remove it only after determining why initialization failed.

Normal service

serve requires no Matrix password, access-token file, declarative device-key pins, or initial cursor:

WEBHOOK_AUTH_TOKEN='matrix-to-n8n-secret' \
matrix-relay-bot serve \
  --matrix-homeserver-url https://matrix.example.test \
  --matrix-bot-user-id @relay:example.test \
  --matrix-device-id N8NRELAY02 \
  --matrix-storage-dir /var/lib/matrix-relay-bot/rust \
  --matrix-room-allowlist '!room:example.test' \
  --n8n-webhook-url https://n8n.example.test/webhook/matrix \
  --bot-send-token-file /run/credentials/matrix-relay-bot.service/bot-send-token \
  --operator-token-file /run/credentials/matrix-relay-bot.service/operator-token \
  --bot-send-bind-address 127.0.0.1 \
  --bot-send-port 8080 \
  --max-download-bytes 52428800

MATRIX_ROOM_ALLOWLIST is optional. When omitted, every joined room is eligible; startup fails if no joined room exists. Matrix SDK encrypts messages in encrypted rooms and preserves normal Matrix behavior in unencrypted rooms. The durable outbox resumes from its stored cursor and does not replay acknowledged events.

HTTP API

Health is unauthenticated:

curl --fail http://127.0.0.1:8080/healthz

Send text to Matrix with the exact BOT_SEND_TOKEN value in the Authorization header:

curl --fail \
  -H 'Authorization: n8n-to-matrix-secret' \
  -H 'Content-Type: application/json' \
  --data '{"roomId":"!room:example.test","message":"hello"}' \
  http://127.0.0.1:8080/send

Successful response:

{"eventId":"$event:example.test"}

For media, replace message with mediaUrl, kind (image, video, audio, or file), and filename. Optional reply fields are replyToEventId, replyToSenderId, and replyToBody.

Webhook

A qualifying decrypted mention is posted to N8N_WEBHOOK_URL:

{
  "text": "cleaned mention text",
  "originalContent": "@relay:example.test do this",
  "roomId": "!room:example.test",
  "eventId": "$event:example.test",
  "senderId": "@alice:example.test",
  "timestamp": 1770000000000,
  "formattedBody": null
}

When WEBHOOK_AUTH_TOKEN is set, the webhook request uses Authorization: Bearer <token>. Successful delivery is durable. HTTP 429, HTTP 5xx, and transport failures receive bounded retries; other non-success responses are discarded as permanent failures.

License

MIT