Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
29 KiB
QR Sign-In via Matrix Authentication Service — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Enable "sign in with QR code" (MSC4108) for Matrix accounts on hobbs.farm, which requires deploying Matrix Authentication Service (MAS) as the homeserver's auth provider.
Architecture: QR sign-in is an OAuth 2.0 device grant, not a password login. Synapse 1.159.0 refuses to start with msc4108_enabled: true unless matrix_authentication_service.enabled is also true. MAS requires PostgreSQL, and — critically — syn2mas can only read a Synapse whose own database is PostgreSQL. Synapse here is on SQLite, so this is a three-stage chain: port Synapse to PostgreSQL, deploy MAS and migrate the accounts into it, then turn on the QR MSCs.
Tech Stack: Synapse 1.159.0 (SQLite → PostgreSQL 16), Matrix Authentication Service (Docker, ghcr.io/element-hq), PostgreSQL 16-alpine ×2, Traefik, Cloudflare DNS, Docker Compose on hf-pdocker-01.
Why this is three migrations, not a config flag
Verified in the running image's own source (synapse/config/experimental.py):
if (self.msc4108_enabled or self.msc4108_delegation_endpoint is not None) and not auth_delegated:
raise ConfigError("MSC4108 requires matrix_authentication_service to be enabled", ...)
The same guard exists for MSC4388 (the QR out-of-band channel). And from the MAS docs:
"It is worth noting that MAS currently only supports PostgreSQL as a database backend." "The migration tool only supports reading from PostgreSQL for the Synapse database as well."
Synapse on this node currently runs database: {name: sqlite3, database: /data/homeserver.db}.
Global Constraints
- Synapse image stays pinned at
ghcr.io/element-hq/synapse:v1.159.0(vprefix required;:1.159.0404s). Do not bump during this work. - Synapse ≥1.143 requires PostgreSQL 14+. Use
postgres:16-alpine. - Synapse's PostgreSQL database MUST be created with
ENCODING 'UTF8' LC_COLLATE 'C' LC_CTYPE 'C'— Synapse refuses any other collation. - MAS requires its own separate, blank PostgreSQL database. Do not share Synapse's.
- All lock-prone databases go on LOCAL disk, never NFS (CLAUDE.md Known Issue #16). Use
/var/lib/synapse-dband/var/lib/mas-db. - On hf-pdocker-01,
memswap_limitmust equalmem_limiton every container (Known Issue #12 — a drift here crashed the whole node on 2026-07-12). - Node headroom: ~8GB available of 23GB, Frigate is the main consumer. Budget ≤600MB total for the new containers.
- hobbs.farm has no wildcard DNS record (memory
hobbs-farm-dns). Traefik uses DNS-01, so a certificate will be issued for a hostname with no A record — a valid cert does NOT mean the name resolves. Any new public hostname needs an explicit Cloudflare A record →98.160.103.126, DNS-only (grey cloud). - Watchtower does not run on .192; pin every image tag explicitly.
- Synapse data lives at
/var/lib/synapseon local NVMe. The NFS copy at/mnt/docker-data/configs/matrix/datais a stale backup — never remount it. - The mandatory post-change checklist in CLAUDE.md applies (CLAUDE.md, MEMORY.md, Prometheus probes, wiki, Farm Manager links).
Blast Radius
Authentication for all 19 accounts moves to MAS. Known consumers that must keep working:
| Account | How it authenticates | Migrates via |
|---|---|---|
@jeremy |
password bigdog01 + 5 Element devices (2× Element X Android, 2× Element Desktop) |
password hash + devices |
@frigate |
long-lived access token syt_ZnJpZ2F0… in frigate-notify/.env |
access token |
@alertbot |
long-lived access token syt_YWxlcnRib3Q… in matrix-receiver-config.yml |
access token |
@margaret |
openclaw.json contains both accessToken and password |
both |
@spirit |
openclaw config (path differs from margaret's — resolve in Task 1) | TBD in Task 1 |
@clippyauto |
n8n credential on .100, polls /sync every ~2s |
TBD in Task 1 |
@fry, @hermes, @julie, @pluto, @testbot, @admin1, @adminuser |
unknown | TBD in Task 1 |
MAS exposes a compatibility layer proxying /_matrix/client/*/login, so password-based bots are expected to keep working — this is the single biggest assumption in the plan and Task 1 exists to test it, not trust it.
File Structure
- Create
compose/hf-pdocker-01/mas/docker-compose.yml— MAS +mas-dbPostgreSQL. One file, one responsibility: authentication. - Modify
compose/hf-pdocker-01/matrix/docker-compose.yml— re-add asynapse-dbservice (PostgreSQL 16, local disk). Note this partly reverses the 2026-08-20 removal of the empty postgres; this one will actually hold data. - Create
/var/lib/synapse/homeserver-postgres.yamlon the node — port-target config forsynapse_port_db(node-only, not in repo — contains DB credentials). - Create
/mnt/docker-data/configs/mas/config.yamlon the node — MAS config (node-only, contains signing keys and secrets; back up to Vaultwarden). - Modify
/mnt/docker-data/traefik/dynamic/temp-routes.yml— add theauth.hobbs.farmrouter + service. (Yes, the Matrix, Home Assistant and Guacamole routes all live in a file calledtemp-routes.yml. It is load-bearing despite the name.) - Modify
CLAUDE.md,MEMORY.md— post-change checklist.
Task 1: Auth inventory and full backup
Files:
- Create:
/var/lib/synapse.bak-premas/(node),docs/superpowers/plans/mas-auth-inventory.md(repo)
Interfaces:
-
Produces: a complete table of every account → auth method → config file location, used by Tasks 7 and 10 to verify nothing broke.
-
Step 1: Enumerate every device and its last-seen IP, to know what "still working" means later
ssh glyptic@192.168.86.192 'docker exec synapse python3 -c "
import sqlite3
c=sqlite3.connect(\"file:/var/lib/synapse/homeserver.db?mode=ro\",uri=True)
for u,d,n,ip in c.execute(\"select user_id,device_id,display_name,last_seen_ip from devices order by user_id\"):
print(u,d,n,ip)
"'
Expected: rows for @jeremy (5 devices), plus bot devices for margaret/spirit/clippyauto/frigate/alertbot/fry.
- Step 2: Locate spirit's and clippyauto's credentials (unresolved in planning)
ssh glyptic@192.168.86.192 'docker inspect spirit --format "{{range .Mounts}}{{.Source}} -> {{.Destination}}{{println}}{{end}}"; docker exec spirit sh -c "ls -la /home/glyptic/.openclaw/ 2>/dev/null || find / -name openclaw.json -maxdepth 6 2>/dev/null | head"'
ssh glyptic@192.168.86.100 'docker exec n8n sh -c "printenv | grep -i matrix" 2>/dev/null; echo "---"; docker exec n8n-db psql -U n8n -tc "select name, type from credentials_entity;" 2>/dev/null | head'
Expected: a config path for spirit and a credential type for n8n. Record whether each uses an access token or a password — access tokens migrate untouched; passwords depend on the MAS compat layer.
- Step 3: Write the inventory file
Record one row per account: user id, auth method, exact file path holding the credential, and how to verify it post-migration. Save as docs/superpowers/plans/mas-auth-inventory.md.
- Step 4: Take a cold, complete backup
ssh glyptic@192.168.86.192 '
cd /mnt/docker-data/compose/hf-pdocker-01/matrix && docker compose stop synapse
sudo rsync -a /var/lib/synapse/ /var/lib/synapse.bak-premas/
sudo python3 -c "
import sqlite3;c=sqlite3.connect(\"/var/lib/synapse.bak-premas/homeserver.db\")
print(\"integrity:\", c.execute(\"pragma integrity_check\").fetchone()[0])
print(\"users:\", c.execute(\"select count(*) from users\").fetchone()[0])
print(\"events:\", c.execute(\"select count(*) from events\").fetchone()[0])
"
docker compose start synapse'
Expected: integrity: ok, 19 users, ~93,884 events. Do not proceed if integrity is anything but ok.
- Step 5: Commit the inventory
git add docs/superpowers/plans/mas-auth-inventory.md
git commit -m "docs: matrix auth inventory ahead of MAS migration"
Task 2: PostgreSQL for Synapse (blank, correct collation)
Files:
- Modify:
compose/hf-pdocker-01/matrix/docker-compose.yml
Interfaces:
-
Produces:
synapse-dbreachable atsynapse-db:5432on thematrix-internalnetwork, databasesynapse, usersynapse, C collation. Task 3 ports data into it. -
Step 1: Add the service to the compose file
synapse-db:
image: postgres:16-alpine
container_name: synapse-db
restart: unless-stopped
environment:
- POSTGRES_USER=synapse
- POSTGRES_PASSWORD=<generate: openssl rand -base64 24>
- POSTGRES_DB=synapse
- POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C
volumes:
- /var/lib/synapse-db:/var/lib/postgresql/data # LOCAL disk - Known Issue #16
networks:
- matrix-internal
mem_limit: 512M
memswap_limit: 512M # MUST equal mem_limit - Known Issue #12
labels:
- "com.centurylinklabs.watchtower.enable=false"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U synapse -d synapse"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
logging:
driver: "json-file"
options: {max-size: "50m", max-file: "3"}
Note: no user: "1024:100" — that workaround is only for NFS root_squash. This volume is local.
- Step 2: Deploy and verify collation
scp compose/hf-pdocker-01/matrix/docker-compose.yml glyptic@192.168.86.192:/mnt/docker-data/compose/hf-pdocker-01/matrix/
ssh glyptic@192.168.86.192 'cd /mnt/docker-data/compose/hf-pdocker-01/matrix && docker compose up -d synapse-db && sleep 20 && docker exec synapse-db psql -U synapse -d synapse -c "show lc_collate;"'
Expected: lc_collate = C. If it says anything else, delete /var/lib/synapse-db and redo — Synapse will refuse to start otherwise, and fixing it later means re-porting.
- Step 3: Commit
git add compose/hf-pdocker-01/matrix/docker-compose.yml
git commit -m "feat: add postgres for synapse (local disk, C collation) ahead of MAS"
Task 3: Port Synapse SQLite → PostgreSQL
Files:
- Create:
/var/lib/synapse/homeserver-postgres.yaml(node only) - Modify:
/var/lib/synapse/homeserver.yaml(node only)
Interfaces:
-
Consumes:
synapse-dbfrom Task 2. -
Produces: a Synapse running on PostgreSQL — the precondition
syn2masrequires in Task 7. -
Step 1: Write the port-target config
Copy /var/lib/synapse/homeserver.yaml to homeserver-postgres.yaml and replace the database: block with:
database:
name: psycopg2
args:
user: synapse
password: <the password from Task 2>
dbname: synapse
host: synapse-db
cp_min: 5
cp_max: 10
- Step 2: Stop Synapse (downtime starts)
ssh glyptic@192.168.86.192 'cd /mnt/docker-data/compose/hf-pdocker-01/matrix && docker compose stop synapse'
- Step 3: Run the port
ssh glyptic@192.168.86.192 'docker run --rm --network matrix_matrix-internal \
-v /var/lib/synapse:/data ghcr.io/element-hq/synapse:v1.159.0 \
synapse_port_db --sqlite-database /data/homeserver.db --postgres-config /data/homeserver-postgres.yaml'
Expected: a progress table per-table, ending in Success!. Budget 10–40 minutes for ~94k events on this hardware. If it fails partway, drop and recreate the database before retrying — the port tool is not idempotent against a partially-filled database.
- Step 4: Verify row counts match the source
ssh glyptic@192.168.86.192 'docker exec synapse-db psql -U synapse -d synapse -tc "select (select count(*) from users) as users, (select count(*) from events) as events;"'
Expected: 19 | 93884 (or higher for events if the server ran between backup and port). Must not be lower.
- Step 5: Cut Synapse over
ssh glyptic@192.168.86.192 'sudo cp /var/lib/synapse/homeserver.yaml /var/lib/synapse/homeserver.yaml.bak-presql
sudo cp /var/lib/synapse/homeserver-postgres.yaml /var/lib/synapse/homeserver.yaml
cd /mnt/docker-data/compose/hf-pdocker-01/matrix && docker compose start synapse'
- Step 6: Verify the service on PostgreSQL
ssh glyptic@192.168.86.192 '
for i in $(seq 1 30); do [ "$(docker inspect synapse --format "{{.State.Health.Status}}")" = healthy ] && echo healthy && break; sleep 10; done
curl -s http://localhost:8008/_synapse/admin/v1/server_version; echo
curl -s -X POST http://localhost:8008/_matrix/client/v3/login -H "Content-Type: application/json" \
-d "{\"type\":\"m.login.password\",\"identifier\":{\"type\":\"m.id.user\",\"user\":\"jeremy\"},\"password\":\"bigdog01\"}" | head -c 80
docker logs synapse --since 5m 2>&1 | grep -cE " ERROR "'
Expected: healthy, version 1.159.0, an access_token in the login response, 0 errors. Log out that verification session afterwards (POST /_matrix/client/v3/logout).
- Step 7: Confirm bots reconnected before declaring the task done
ssh glyptic@192.168.86.192 'sleep 60; docker logs synapse --since 60s 2>&1 | grep -oE "\{@[a-z0-9]+:hobbs.farm\}" | sort | uniq -c'
Expected: clippyauto, margaret, spirit, fry all present. Rollback if not: restore homeserver.yaml.bak-presql and restart — SQLite file is untouched and still authoritative until Task 7.
- Step 8: Commit
git commit --allow-empty -m "chore: synapse ported from sqlite to postgres (node config, not in repo)"
Task 4: PostgreSQL for MAS
Files:
- Create:
compose/hf-pdocker-01/mas/docker-compose.yml
Interfaces:
-
Produces:
mas-db:5432, databasemas, usermas, on a newmasbridge network that also carries the MAS container. -
Step 1: Create the compose file with the database service only
Same shape as Task 2 Step 1, but: container_name: mas-db, POSTGRES_USER=mas, POSTGRES_DB=mas, volume /var/lib/mas-db:/var/lib/postgresql/data, mem_limit: 256M, memswap_limit: 256M, network mas. Default collation is fine — the C-collation requirement is Synapse's, not MAS's.
- Step 2: Deploy and verify
ssh glyptic@192.168.86.192 'cd /mnt/docker-data/compose/hf-pdocker-01/mas && docker compose up -d mas-db && sleep 20 && docker exec mas-db psql -U mas -d mas -tc "select 1;"'
Expected: 1.
- Step 3: Commit
git add compose/hf-pdocker-01/mas/docker-compose.yml
git commit -m "feat: add mas-db postgres for matrix authentication service"
Task 5: Generate MAS config and deploy the service
Files:
- Create:
/mnt/docker-data/configs/mas/config.yaml(node only — contains secrets) - Modify:
compose/hf-pdocker-01/mas/docker-compose.yml
Interfaces:
-
Consumes:
mas-dbfrom Task 4. -
Produces: MAS listening on host port 8020 (verified free on .192: 8008/8010/8080/8082/8085/8086/8090/8123/8181/8199/8300/8310 are taken).
-
Step 1: Generate a config seeded from the Synapse config
ssh glyptic@192.168.86.192 'sudo mkdir -p /mnt/docker-data/configs/mas && docker run --rm \
-v /var/lib/synapse:/synapse:ro -v /mnt/docker-data/configs/mas:/out \
ghcr.io/element-hq/matrix-authentication-service:latest \
config generate --synapse-config /synapse/homeserver.yaml --output /out/config.yaml'
Expected: config.yaml written, containing generated signing keys and a matrix.secret shared secret.
- Step 2: Set the database URI, public base, and password hashing scheme
Edit /mnt/docker-data/configs/mas/config.yaml:
database:
uri: postgresql://mas:<password>@mas-db/mas
http:
public_base: https://auth.hobbs.farm
listeners:
- name: web
resources: [{name: discovery}, {name: human}, {name: oauth}, {name: compat}, {name: graphql}, {name: assets}]
binds: [{address: "[::]:8080"}]
passwords:
enabled: true
schemes:
- version: 1
algorithm: bcrypt
unicode_normalization: true # REQUIRED for imported Synapse hashes
- version: 2
algorithm: argon2id
The unicode_normalization: true on the bcrypt v1 scheme is what makes migrated Synapse password hashes verify. Without it, every migrated password silently fails.
- Step 3: Add the MAS service to compose
mas:
image: ghcr.io/element-hq/matrix-authentication-service:latest # pin to the digest after first pull
container_name: mas
restart: unless-stopped
command: ["server", "--config", "/config/config.yaml"]
volumes:
- /mnt/docker-data/configs/mas:/config:ro
ports:
- "8020:8080"
networks: [mas, matrix-internal]
depends_on:
mas-db: {condition: service_healthy}
mem_limit: 256M
memswap_limit: 256M
labels:
- "com.centurylinklabs.watchtower.enable=false"
MAS needs to reach Synapse's admin API and Synapse needs to reach MAS, hence both networks.
- Step 4: Run migrations and start
ssh glyptic@192.168.86.192 'cd /mnt/docker-data/compose/hf-pdocker-01/mas && docker compose run --rm mas database migrate --config /config/config.yaml && docker compose up -d mas && sleep 15 && curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8020/health'
Expected: migrations apply cleanly, health endpoint returns 200.
- Step 5: Store the MAS secrets in Vaultwarden and commit
The generated config.yaml holds signing keys that cannot be regenerated without invalidating sessions. Store a copy in Vaultwarden (note: the vault master password is currently unknown — see memory vault-master-password-stale; resolve that first or keep an encrypted copy on the NAS).
git add compose/hf-pdocker-01/mas/docker-compose.yml
git commit -m "feat: deploy matrix authentication service on :8020"
Task 6: syn2mas check and dry-run
Interfaces:
-
Consumes: Synapse-on-PostgreSQL (Task 3), MAS + DB (Tasks 4–5).
-
Produces: a clean dry-run, the go/no-go gate for Task 7.
-
Step 1: Run the checker
ssh glyptic@192.168.86.192 'docker exec mas mas-cli syn2mas check --config /config/config.yaml --synapse-config /synapse/homeserver.yaml'
(Mount /var/lib/synapse read-only into the MAS container as /synapse for this — add it in Task 5 Step 3 if not already present.)
Expected: no blocking findings. Known blockers it looks for: custom password provider modules, SAML/LDAP providers. This deployment has neither.
- Step 2: Dry run
ssh glyptic@192.168.86.192 'docker exec mas mas-cli syn2mas migrate --config /config/config.yaml --synapse-config /synapse/homeserver.yaml --dry-run'
Expected: a summary of users, password hashes, devices, and access tokens it would import. Cross-check the user count against 19 and the device count against Task 1 Step 1. Do not proceed to Task 7 if the counts disagree.
- Step 3: Record the dry-run output in the inventory doc and commit
git add docs/superpowers/plans/mas-auth-inventory.md
git commit -m "docs: syn2mas dry-run results"
Task 7: Migrate accounts into MAS and delegate Synapse auth
Files:
- Modify:
/var/lib/synapse/homeserver.yaml(node only)
Interfaces:
- Produces: Synapse delegating all authentication to MAS — the precondition MSC4108 requires.
This is the point of no return. The MAS docs state migration "is not easily reversible and requires careful backup procedures before proceeding." Rollback means restoring both databases from backup.
- Step 1: Fresh backup of both databases immediately before migrating
ssh glyptic@192.168.86.192 '
cd /mnt/docker-data/compose/hf-pdocker-01/matrix && docker compose stop synapse
docker exec synapse-db pg_dump -U synapse synapse | gzip > /tmp/synapse-premas.sql.gz
docker exec mas-db pg_dump -U mas mas | gzip > /tmp/mas-premigrate.sql.gz
sudo mv /tmp/*.gz /var/lib/synapse.bak-premas/ && ls -la /var/lib/synapse.bak-premas/*.gz'
Expected: two non-empty .gz files.
- Step 2: Run the migration (Synapse stays stopped)
ssh glyptic@192.168.86.192 'docker exec mas mas-cli syn2mas migrate --config /config/config.yaml --synapse-config /synapse/homeserver.yaml'
Expected: completes with an import summary matching the dry-run.
- Step 3: Point Synapse at MAS
Add to /var/lib/synapse/homeserver.yaml:
matrix_authentication_service:
enabled: true
endpoint: http://mas:8080/
secret: <the matrix.secret value from /mnt/docker-data/configs/mas/config.yaml>
- Step 4: Start Synapse and verify delegation is live
ssh glyptic@192.168.86.192 '
cd /mnt/docker-data/compose/hf-pdocker-01/matrix && docker compose start synapse
for i in $(seq 1 30); do [ "$(docker inspect synapse --format "{{.State.Health.Status}}")" = healthy ] && echo healthy && break; sleep 10; done
curl -s -o /dev/null -w "auth_metadata: %{http_code}\n" http://localhost:8008/_matrix/client/unstable/org.matrix.msc2965/auth_metadata'
Expected: healthy, and auth_metadata now returns 200 (it returns 404 today).
- Step 5: Verify every account from the Task 1 inventory still authenticates
ssh glyptic@192.168.86.192 '
echo "--- password login via MAS compat layer:"
curl -s -X POST http://localhost:8008/_matrix/client/v3/login -H "Content-Type: application/json" \
-d "{\"type\":\"m.login.password\",\"identifier\":{\"type\":\"m.id.user\",\"user\":\"jeremy\"},\"password\":\"bigdog01\"}" | head -c 100; echo
echo "--- migrated access token (frigate):"
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8008/_matrix/client/v3/account/whoami -H "Authorization: Bearer syt_ZnJpZ2F0<rest-from-.env>"
echo "--- bots syncing:"
sleep 90; docker logs synapse --since 90s 2>&1 | grep -oE "\{@[a-z0-9]+:hobbs.farm\}" | sort | uniq -c'
Expected: an access_token for jeremy, 200 for the frigate token, and margaret/spirit/clippyauto/fry all syncing. If password login fails, the bcrypt unicode_normalization: true setting from Task 5 Step 2 is the first thing to check.
- Step 6: Commit
git commit --allow-empty -m "feat: synapse auth delegated to MAS (node config)"
Task 8: Publish MAS and add DNS
Files:
-
Modify:
/mnt/docker-data/traefik/dynamic/temp-routes.yml(confirmed: this is the file holding themess,wellknown,yodaandsolorouters) -
External: Cloudflare DNS
-
Step 1: Add the Cloudflare A record FIRST
auth.hobbs.farm → 98.160.103.126, DNS-only (grey cloud). There is no wildcard record on this zone, and Traefik's DNS-01 challenge will happily issue a valid certificate for a hostname that does not resolve — so a working cert proves nothing here (memory hobbs-farm-dns).
Verify from outside the LAN before continuing:
getent hosts auth.hobbs.farm
Expected: 98.160.103.126.
- Step 2: Add the Traefik router and service
auth:
rule: "Host(`auth.hobbs.farm`)"
service: auth
entryPoints: [websecure]
middlewares: [security-headers]
tls: {certResolver: letsencrypt}
auth:
loadBalancer:
servers:
- url: "http://192.168.86.192:8020"
Do not add basicAuth — clients must reach this unauthenticated to log in.
- Step 3: Verify publicly
curl -s -o /dev/null -w "%{http_code}\n" https://auth.hobbs.farm/health
curl -s https://mess.hobbs.farm/_matrix/client/unstable/org.matrix.msc2965/auth_metadata | head -c 300
Expected: 200, and auth metadata advertising https://auth.hobbs.farm endpoints.
- Step 4: Confirm
.well-knownneeds no change (verify, do not assume)
Current contents, served from /mnt/docker-data/configs/matrix/wellknown/ via matrix-wellknown on :8010:
client -> {"m.homeserver": {"base_url": "https://mess.hobbs.farm"}}
server -> {"m.server": "mess.hobbs.farm:443"}
Under MAS, clients discover the auth server from Synapse's auth_metadata endpoint, not from .well-known, so both files should stay as they are. Verify a client can complete discovery end to end:
curl -s https://hobbs.farm/.well-known/matrix/client
curl -s https://mess.hobbs.farm/_matrix/client/unstable/org.matrix.msc2965/auth_metadata | head -c 200
Expected: .well-known unchanged and still pointing at mess.hobbs.farm; auth_metadata returning the auth.hobbs.farm issuer. If a client cannot find the auth server, revisit http.public_base in the MAS config (Task 5 Step 2) before touching .well-known.
- Step 5: Add a Prometheus blackbox probe for the new endpoint
Add https://auth.hobbs.farm/health to the blackbox target list in /mnt/docker-data/configs/prometheus/prometheus.yml, then hot-reload:
ssh glyptic@192.168.86.192 'curl -s -X POST http://localhost:9095/-/reload && sleep 5 && curl -s "http://localhost:9095/api/v1/query?query=probe_success" | grep -o "auth.hobbs.farm"'
Expected: the target appears and probes 1. Auth being down now means nobody can log in — this probe is not optional.
- Step 6: Commit
git add compose/ && git commit -m "feat: publish MAS at auth.hobbs.farm with blackbox probe"
Task 9: Enable the QR MSCs
Files:
-
Modify:
/var/lib/synapse/homeserver.yaml(node only) -
Step 1: Enable both MSCs
experimental_features:
msc4108_enabled: true
msc4388_mode: authenticated
msc4388_mode: authenticated requires the rendezvous channel user to be logged in — prefer it over open, which would let unauthenticated parties open rendezvous sessions on an internet-facing server.
- Step 2: Restart and confirm Synapse actually starts
ssh glyptic@192.168.86.192 'docker restart synapse && sleep 45 && docker logs synapse --since 2m 2>&1 | grep -iE "ConfigError|MSC4108|MSC4388" | head'
Expected: no ConfigError. If you see MSC4108 requires matrix_authentication_service to be enabled, Task 7 Step 3 did not take effect.
- Step 3: Verify the feature is advertised
ssh glyptic@192.168.86.192 'curl -s http://localhost:8008/_matrix/client/versions | python3 -c "
import sys,json; uf=json.load(sys.stdin)[\"unstable_features\"]
print(\"msc4108:\", uf.get(\"org.matrix.msc4108\"))
"'
Expected: msc4108: True (it is False today).
- Step 4: Commit
git commit --allow-empty -m "feat: enable MSC4108/MSC4388 QR sign-in"
Task 10: End-to-end QR sign-in test and documentation
- Step 1: Test the actual feature
On an already-signed-in Element X Android device: Settings → Sessions → "Sign in with QR code" / "Link new device". On a second client, choose "Sign in with QR code" and scan.
Expected: the new device signs in and receives E2EE keys without the password being typed. This is the deliverable — if it does not work here, the feature is not done regardless of what the config says.
- Step 2: Re-verify the full account inventory one more time
Re-run Task 7 Step 5. Expected: unchanged — all bots syncing, all tokens valid.
-
Step 3: Complete the mandatory post-change checklist (CLAUDE.md)
-
CLAUDE.md: MAS + two PostgreSQL containers on .192, port 8020,
auth.hobbs.farm, Synapse now on PostgreSQL (supersedes Known Issues #24/#25 wording), new known issue for the MAS dependency — auth is now a hard dependency for every login, including bots -
MEMORY.md: new entry for the MAS migration; update
matrix-account(the reset recipe changes — user management moves to MAS) -
Prometheus: done in Task 8 Step 4
-
Farm Manager links page: add
auth.hobbs.farmtoserverLinksin/opt/farm-manager/server/static/app.js, then rebuild the image (links are baked in, not mounted) and redeploy -
Wiki: run the documentation-keeper agent
-
Step 4: Retire the backups once stable
Leave /var/lib/synapse.bak-premas/, /var/lib/synapse.bak-20260820-pre1159/, and the SQLite file in place for at least a week of normal use. Only then delete.
- Step 5: Final commit
git add -A && git commit -m "docs: MAS QR sign-in deployment complete"
Rollback Summary
| Failed at | Recovery |
|---|---|
| Task 3 (port to PG) | Restore homeserver.yaml.bak-presql; SQLite file untouched and still authoritative |
| Task 7 (syn2mas) | Restore synapse-premas.sql.gz + mas-premigrate.sql.gz; remove matrix_authentication_service block |
| Task 9 (MSCs) | Remove experimental_features block; restart — no data implications |
Open Questions for the Operator
- Element Desktop compatibility. Your two Element Desktop sessions are on the legacy auth stack. OIDC-native support in Element Web/Desktop has historically lagged Element X. Worth confirming before migrating, since a broken desktop client is a worse day than no QR code.
- Is
auth.hobbs.farmthe hostname you want? It becomes permanent — it is baked into session state, and changingpublic_baselater invalidates sessions. - Vaultwarden is currently locked out (memory
vault-master-password-stale). MAS generates signing keys that must be backed up somewhere durable. Resolve the vault access first, or agree on an alternative. - Node memory. .192 sits at ~14/23GB with Frigate as the main consumer. This adds ~1GB across three containers (Synapse's PG, MAS's PG, MAS). If that is too tight, the MAS stack could go on .100 instead — it only needs network reachability to Synapse, not co-location.