svccache: live-tail local cache of at.sovrn.mail.service records
opensvccache: 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/jetstreamGo 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=selfupserts (appview,webmail,cid,seq); apply only ifseq> storedseq(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), tablessvc_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
NewAppalongside the verifier;RecordReadergains aLocalCacheseam (small refactor of the Task-4 code, done here not earlier); our ownEnsurewrites 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)