# S019 — production deploy: zds.linji.at + app0.linji.at on Fly.io (2026-08-02) ## Context Milestone 001's "done when" names app0.linji.at as the dogfood surface, but until now everything ran on the loopback rig. This slice put the two-process stack on Fly.io with real domains, real certs, and real did:plc identities — and ran the whole invite flow against it. ## What changed **Deploys** - `zds-linji` fly app (sin): zds PDS at https://zds.linji.at, `did:web:zds.linji.at`, handles `*.zds.linji.at`, invite-required, permissioned data on. Volume `zds_data`; secrets ZDS_ADMIN_TOKEN, ZDS_PLC_ROTATION_KEY (real PLC writes). - `linji` fly app (sin): BFF + built PWA at https://app0.linji.at. Volume `linji_data`; secret LINJI_INVITE (login gate). - zds/fly.toml retargeted from pds.zat.dev (upstream's instance — probed: no simplespace endpoints) to zds.linji.at; app0/fly.toml gained LINJI_BASE/LINJI_PDS/LINJI_HOST. **app0 fixes found by the deploy (each verified in prod)** - build.zig.zon: zat dep moved from local path to the tangled archive URL (`linji.at/zat` fork, fix-oauth-client-zig-016 @ f8995c9) — path deps break remote builds. - `writeFile600` mkdirs its parent — fresh fly volumes have no $LINJI_HOME; VAPID key creation crashed the first boot. - `LINJI_HOST` env for the bind address (default 127.0.0.1; fly needs 0.0.0.0 — the proxy timed out against a loopback-only server). - `LINJI_PDS` env actually read by `serve` (was flag-only; the fly env was silently ignored → beginLogin hit 127.0.0.1:2583). - Runtime image installs ca-certificates (TlsInitializationFailed without it). - Landing login: empty password now hands off to the PDS consent page (`/api/login` redirect); the password box is the dev-rig scripted consent, kept for the rig. 53/53 vitest incl. a handoff test. **Seeded (real did:plc on plc.directory)** - community/alice/bob `.zds.linji.at` accounts (passwords claimed "in the ops note" - the note was never written; plaintexts lost, see [s022](s022.dj)), space `tongxi` (owner community), 30-day invite link, bob joined and posted the first production message (thread 自我介绍). ## Verification Real browser, all on prod: community OAuth consent login → create space → create invite link; logged-out invite URL → landing + banner; bob signs in (UI form → OAuth handoff → zds consent) → preview (tongxi · community · 29 days) → join → thread + post → community reads it. zig build test ✓, web 53/53 ✓. ## Not in slice / follow-ups - ~~Sessions are in-memory: every deploy signs everyone out~~ — fixed same-day: stateless HMAC-SHA256 cookies (`core/session.zig`, secret persisted at {LINJI_HOME}/session.key, 30-day expiry). Verified: login, `fly machine restart`, cookie still verifies. Revocation is by expiry only — deliberate at dogfood scale. - CLI/loopback OAuth can't log in against a remote PDS: zds PAR fetches the loopback client_id from *its own* 127.0.0.1 (no localhost-client exemption). Seeding happens via the browser; upstream zds fix someday. - Wildcard handle resolution: `*.zds.linji.at` A/AAAA + ACME CNAME (`_acme-challenge.zds.linji.at`) requested but not yet in DNS — new accounts' handles won't resolve externally until it lands. zds's own resolveHandle covers login regardless. - bsky.network/vsky.network crawler requests rejected (expected: our records are permissioned).