svccache: live-tail local cache of at.sovrn.mail.service records

open
#0ffe23b opened by agent Sep 16

svccache: live-tail local cache of at.sovrn.mail.service records

Follow-up to bug a472ea3 (transparent cell redirect on OAuth login). The login redirect reads the user’s service record from their PDS on every login — one error-prone network hop. A local cache built from the jetstream eliminates the PDS fetch on cache hits.

Agreed scope (2026-09-16)

  • Consumer: github.com/bluesky-social/jetstream Go client, WithCollections(["at.sovrn.mail.service"]) + WithKinds([KindCommit]), pure live tail — no archive replay, no API key. Cold box starts empty and warms organically (logins + our own writes).
  • Fold rules (keyed by DID only, never handle): create/update with rkey=self upserts (appview, webmail, cid, seq); apply only if seq > stored seq (out-of-order guard); delete op removes the row; account/identity deactivate events purge the DID (they bypass the collection filter upstream, so they arrive free).
  • Trust model: hits trusted unconditionally (no freshness gate). Records ~never change, so a hit is correct regardless of consumer lag; the only mutation vector is cell migration, a coordinated runbook event. Misses ALWAYS fall back to live PDS fetch — cold/dead consumers and uncrawled PDSes degrade to today’s path, never to a wrong answer. Miss ≠ new user.
  • Store: per-cell sqlite svccache.db (WAL, repo conventions), tables svc_records(did PK, appview, webmail, cid, seq, observed_at) + svc_cursor(id, seq, updated_at). Explicitly excluded from Litestream backup; rebuild = delete file, restart.
  • Wiring: background goroutine in NewApp alongside the verifier; RecordReader gains a LocalCache seam (small refactor of the Task-4 code, done here not earlier); our own Ensure writes upsert directly.
  • Observability: connection/ping liveness for ops alerting (event recency is NOT a health signal — quiet weekends look identical to a stall); optional healthz leg later.
  • Migration runbook: purge all cells’ caches as a migration step. Ordering vs. the record rewrite doesn’t matter for correctness (purge → miss → fetch is always right); purge-before-migrate lets the migration commits re-warm live consumers.
  • Non-goals: no backfill/replay, no handle-keyed rows, no shared cross-cell store, no changes to the fail-closed error taxonomy (503 retry / 500 support / RecordNotFound-only fall-through).

Basis

  • Design: docs/plans/2026-09-16-cell-redirect-login-design.md
  • Upstream client: github.com/bluesky-social/jetstream (filter.go, options.go — WithCollections, WithKinds, WithLiveCursor, collection-filter bypass for DID-level events)
  • Existing cursor/gap conventions: docs/03 §C1/C2, docs/01 §7 (DEGRADED, cursor freshness probes) — reuse the patterns, not the code (watcher is identity-events; this is a collection-specific consumer; extract shared bits only when the watcher lands — YAGNI until then)