--- id: appview title: lance.blue shows a match it never hosted status: open repos: [headquarters, infra] dependsOn: [match-records, managed-store] exitCriterion: > A match written by a client that is not ours appears on the site. --- # appview lexicons' stage 6. Everything up to here reads from our own database; this reads from the network. It is the first thing in the plan that is a new service rather than a schema, and the first with an operational cost of its own. Observe is the masthead label for what a reader does with it — matches other people played, ours or not. It is on the bar already, in the community group beside [Leaderboards](leaderboard.md), in the under-construction state: named, dimmed, not a link, and with no address behind it. ## Ingest - [ ] **Filter a stream to our collections.** Jetstream takes a `wantedCollections` list and supports whole-segment prefix wildcards, so `blue.lance.*` and `games.permadeath.*` are two filters rather than fifteen. Volume for collections this small will be dominated by keepalives. - [ ] **Detect a stale cursor yourself.** The retention window is on the order of a day and a half, and a cursor older than that is silently clamped with no error frame. The result is a gap with no signal. Compare the first event received against the cursor asked for. - [ ] **Assume at-most-once delivery.** Jetstream events carry no signatures and no sequence number, so a dropped event is undetectable from inside the stream. Treat it as a fast path and reconciliation as the correctness mechanism, not the other way round. - [ ] **Deduplicate on the per-repo revision, not on a stream offset.** Sequence numbers are specific to one relay instance and are not portable between them. - [ ] **Handle account and identity events**, which arrive regardless of the collection filter. They are the only signal for a handle change or a deleted account. ## Backfill - [ ] **Enumerate repos that already hold our collections.** `com.atproto.sync.listReposByCollection` answers exactly this and is served by relays rather than by PDSes. For a collection this niche the whole list is a few requests. - [ ] **Fetch each repo's existing records** and reconcile against what the live stream has already delivered. Ordering matters: historical data must finish landing before live events for the same repo are treated as authoritative. - [ ] **Accept that the picture is incomplete.** A PDS offline during backfill is a hole, and relays do not archive. That is a property of the network rather than a bug, and `leaderboard` has to state it rather than paper over it. ## Operations - [ ] **Somewhere to run it** (infra). A long-lived process with a cursor, so it needs durable state — the same conversation as [managed-store](managed-store.md), which is why that is a dependency. - [ ] **A read API** for the site: a player's matches, a match by id, matches involving two players. - [ ] **Decide what it stores.** Full records, or an index pointing back at PDSes. The index is smaller and stays honest about where the data lives; full copies survive a PDS disappearing, which `leaderboard` cares about. ## What the bar says about it - [ ] **Decide whether Observe carries a live count.** "3 matches running now" beside the label is the cheapest signal this site has that anybody else is here, and on a quiet site that is worth more than the page it links to. The live registry already knows the number; the appview is what makes it more than our own matches. The reason it is a decision and not a task: zero is louder than no number. A bar that says 0 every evening tells a visitor something true and discouraging, and it does it on every page. Options are a floor (show nothing under some count), a window ("12 today" rather than "0 now"), or accepting the honesty. Pick before building, because the version that shows a count and then quietly stops showing it is the worst of the three. ## Saying what the records mean - [ ] **The schemas are not documented anywhere a stranger reads.** A page on the site naming each `blue.lance.*` and `games.permadeath.*` collection, what it carries and how to read one, is what makes "a client that is not ours" something somebody could actually write. It belongs after this epic rather than before it: the schemas are deliverables of whichever epic needs them, and documenting a collection name before records exist in other people's repositories advertises something that can still change. ## What it unlocks - Matches and challenges written by clients that are not ours. This is the point of putting records in PDSes at all, and until this exists none of it is exercised. - Sanctions from authorities that are not us — a tournament organiser sanctioning a match becomes visible with no coordination. - The second wave of [achievements](achievements.md), which need a player's whole history rather than one match. - Somebody else's profile page ([player-profile](player-profile.md)), for a player whose matches we did not host. ## Done Nothing closed yet.