Matrix
Self-hosted Matrix homeserver (Synapse) with an E2EE bot adapter serving as the bridge between Adolf/Zabbix and Matrix rooms.
Synapse
Homeserver: mtx.alogins.net (Caddy → Synapse container port 8008)
Compose directory: agap_git/matrix/
Creating New Users
Registration is disabled by default. Use register_new_matrix_user with the shared secret from homeserver.yaml.
docker exec -it synapse register_new_matrix_user \
-u <username> \
-p <password> \
-c /data/homeserver.yaml \
http://localhost:8008
Add --admin flag to create an admin user. The shared secret is in ~/agap_git/matrix/data/synapse/homeserver.yaml under registration_shared_secret.
To create a user non-interactively:
docker exec synapse register_new_matrix_user \
-u <username> \
-p <password> \
--no-admin \
-c /data/homeserver.yaml \
http://localhost:8008
Matrix Bot
Repo: ~/matrixbot/ — http://localhost:3000/alvis/matrixbot (if pushed)
FastAPI service (port 3002) running two matrix-nio E2EE clients:
| Account | Tag | Device ID | Purpose |
|---|---|---|---|
@bot:mtx.alogins.net |
adolf |
ADOLFDEVICE |
Adolf channel adapter — inbound (forwards to deepagents) and outbound |
@zabbix:mtx.alogins.net |
zabbix |
ZABBIXDEVICE |
Zabbix notifications — outbound only |
API Endpoints
| Method | Path | Description |
|---|---|---|
POST |
/send |
Send message as adolf (@bot) — body: {"room_id": "...", "text": "..."} |
POST |
/zabbix/send |
Send message as @zabbix — body: {"room_id": "...", "text": "..."} |
GET |
/health |
Health check |
E2EE and Cross-Signing
Both bots bootstrap cross-signing keys on first startup:
- Upload device keys to homeserver (
keys_upload) - Generate master, self-signing, and user-signing olm key pairs
- Upload via
keys/device_signing/uploadwith UIAA password auth - Self-sign the device key via
keys/signatures/upload - Persist key material to
/data/{adolf,zabbix}/cross_signing.json
Device keys must be uploaded before cross-signing — otherwise the server doesn't know the device and self-signing fails.
On subsequent starts, keys are loaded from the persisted file — no regeneration.
In-Room SAS Verification
Both bots support interactive emoji verification initiated from Element X. The full flow:
Element X Bot
├─ m.key.verification.request ─→
←─ m.key.verification.ready ───┤
├─ m.key.verification.start ──→
←─ m.key.verification.accept ──┤
├─ m.key.verification.key ────→
←─ m.key.verification.key ─────┤
←─ m.key.verification.mac ─────┤
├─ m.key.verification.mac ────→
←─ m.key.verification.done ────┤
├─ m.key.verification.done ───→
The bot auto-accepts emoji matches. Master cross-signing key is included in the MAC so Element X can establish the cross-signing trust chain (green verified star).
Outgoing verification events must NOT contain transaction_id (that field is for to-device only) — only m.relates_to with rel_type: m.reference.
To-device verification is also handled as a fallback.
Crypto Store
E2EE state (olm sessions, megolm group sessions, device keys) is persisted in SQLite databases:
~/matrixbot/data/
├── adolf/@bot:mtx.alogins.net_ADOLFDEVICE.db
├── adolf/cross_signing.json
├── zabbix/@zabbix:mtx.alogins.net_ZABBIXDEVICE.db
└── zabbix/cross_signing.json
| Store | Pickle passphrase |
|---|---|
| SQLite databases (olm/megolm sessions) | DEFAULT_KEY (matrix-nio default) |
cross_signing.json files |
matrixbot-cs-keys (CS_PICKLE_PASS in bot.py) |
To decrypt E2EE messages, run inside the matrixbot container (host python-olm links against a different libolm, causing BAD_ACCOUNT_KEY):
# docker exec matrixbot python3 -c "..."
import olm, sqlite3
conn = sqlite3.connect('/data/zabbix/@zabbix:mtx.alogins.net_ZABBIXDEVICE.db')
cur = conn.cursor()
cur.execute('SELECT session_id, session FROM megolminboundsessions WHERE room_id = ?', (ROOM,))
for sid, blob in cur.fetchall():
session = olm.InboundGroupSession.from_pickle(blob, 'DEFAULT_KEY')
plaintext, idx = session.decrypt(ciphertext)
Rooms
| Room ID | Name |
|---|---|
!kNQXdXrjSAjoAMdosG:mtx.alogins.net |
Agap Notifications (Zabbix) |
!vYXGUTRHUIIrrZXTFE:mtx.alogins.net |
Adolf chat |
Gotchas
- Device key upload before cross-signing:
keys_upload()must run beforebootstrap_cross_signing(), otherwise the server can't find the device for self-signing. - Store corruption on copy: Never copy olm store directories while the bot is running — the olm identity keys will diverge from what the server has. If you need to rename the store directory, stop the container first.
- Changing device ID: If you change the device ID, you must: get a new access token (login with new device_id), clear the store, delete the old device from the server, and re-bootstrap cross-signing. Element X caches device keys aggressively — a new device ID forces a fresh key fetch.
transaction_idin room events: nio'saccept_verification(),share_key(),get_mac()returnToDeviceMessageobjects whose.contentincludestransaction_id. Strip it before sending as a room event — Element X may ignore events with this field.
Environment
Variables in ~/matrixbot/.env, passed through docker-compose.yml:
| Variable | Description |
|---|---|
MATRIX_HOMESERVER |
Synapse URL (internal: http://synapse:8008) |
MATRIX_ADOLF_TOKEN |
@bot access token |
MATRIX_ADOLF_PASSWORD |
@bot password (for UIAA during cross-signing) |
MATRIX_ADOLF_DEVICE_ID |
ADOLFDEVICE |
MATRIX_ZABBIX_TOKEN |
@zabbix access token |
MATRIX_ZABBIX_PASSWORD |
@zabbix password |
MATRIX_ZABBIX_DEVICE_ID |
ZABBIXDEVICE |
DEEPAGENTS_URL |
Adolf deepagents endpoint (http://host.docker.internal:8000) |
Tokens and passwords stored in Vaultwarden: MATRIX_ADOLF_TOKEN, MATRIX_ADOLF_PASSWORD, MATRIX_ZABBIX_TOKEN, MATRIX_ZABBIX_PASSWORD.
Stack
~/matrixbot/
├── bot.py Single-file bot (FastAPI + matrix-nio)
├── docker-compose.yml Service definition, networks: matrix_frontend, zabbix_frontend
├── Dockerfile python:3.12-slim + libolm-dev
├── requirements.txt matrix-nio[e2e]==0.25.2, fastapi, uvicorn, httpx, pydantic
├── .env Tokens and passwords
└── data/ Persisted state (olm sessions, cross-signing keys)
├── adolf/
└── zabbix/
Start
cd ~/matrixbot
docker compose up -d --build
Networks
The container joins two external Docker networks:
matrix_frontend— access to Synapse containerzabbix_frontend— allows Zabbix media type to reach/zabbix/send