# 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`): ```python 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** ```bash 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)** ```bash 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** ```bash 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** ```bash 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** ```yaml synapse-db: image: postgres:16-alpine container_name: synapse-db restart: unless-stopped environment: - POSTGRES_USER=synapse - POSTGRES_PASSWORD= - 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** ```bash 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** ```bash 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: ```yaml database: name: psycopg2 args: user: synapse password: dbname: synapse host: synapse-db cp_min: 5 cp_max: 10 ``` - [ ] **Step 2: Stop Synapse (downtime starts)** ```bash ssh glyptic@192.168.86.192 'cd /mnt/docker-data/compose/hf-pdocker-01/matrix && docker compose stop synapse' ``` - [ ] **Step 3: Run the port** ```bash 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** ```bash 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** ```bash 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** ```bash 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** ```bash 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** ```bash 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** ```bash 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** ```bash 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** ```bash 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`: ```yaml database: uri: postgresql://mas:@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** ```yaml 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** ```bash 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). ```bash 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** ```bash 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** ```bash 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** ```bash 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** ```bash 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)** ```bash 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`: ```yaml matrix_authentication_service: enabled: true endpoint: http://mas:8080/ secret: ``` - [ ] **Step 4: Start Synapse and verify delegation is live** ```bash 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** ```bash 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" 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** ```bash 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: ```bash getent hosts auth.hobbs.farm ``` Expected: `98.160.103.126`. - [ ] **Step 2: Add the Traefik router and service** ```yaml auth: rule: "Host(`auth.hobbs.farm`)" service: auth entryPoints: [websecure] middlewares: [security-headers] tls: {certResolver: letsencrypt} ``` ```yaml 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** ```bash 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: ```bash 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: ```bash 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** ```bash 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** ```yaml 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** ```bash 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** ```bash 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** ```bash 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** ```bash 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.