Files
hobbs-iac/docs/superpowers/plans/2026-08-20-mas-qr-login.md
T
2026-08-20 14:57:55 -05:00

29 KiB
Raw Blame History

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 (v prefix required; :1.159.0 404s). 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-db and /var/lib/mas-db.
  • On hf-pdocker-01, memswap_limit must equal mem_limit on 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/synapse on local NVMe. The NFS copy at /mnt/docker-data/configs/matrix/data is 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-db PostgreSQL. One file, one responsibility: authentication.
  • Modify compose/hf-pdocker-01/matrix/docker-compose.yml — re-add a synapse-db service (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.yaml on the node — port-target config for synapse_port_db (node-only, not in repo — contains DB credentials).
  • Create /mnt/docker-data/configs/mas/config.yaml on 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 the auth.hobbs.farm router + service. (Yes, the Matrix, Home Assistant and Guacamole routes all live in a file called temp-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-db reachable at synapse-db:5432 on the matrix-internal network, database synapse, user synapse, 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-db from Task 2.

  • Produces: a Synapse running on PostgreSQL — the precondition syn2mas requires 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, database mas, user mas, on a new mas bridge 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-db from 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 the mess, wellknown, yoda and solo routers)

  • 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-known needs 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.farm to serverLinks in /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

  1. 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.
  2. Is auth.hobbs.farm the hostname you want? It becomes permanent — it is baked into session state, and changing public_base later invalidates sessions.
  3. 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.
  4. 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.