# ADR — Discovery mechanism (inbox-fed index, curator, fan-out) **Date:** 2026-06-16 · **Status:** mechanism accepted; target owner undecided. Ported here for the **discovery MECHANISM** it defines — the piece this lib realizes (`inbox.ts` post/materialize/watch; `store-registry.ts` fan-out). The product intent (what a consumer *should* surface) is the consumer's concern, not this lib's; only the mechanism is recorded here. ## Access ≠ discovery - **Access**: may I read this document if I hold it? A public entity is world-readable with its NURI. - **Discovery**: how do I learn it exists, in order to read it? ← this ADR. ## The mechanism 1. **A single global index**, **fed via ITS inbox**. The creator does **not** edit the index directly: it **deposits a reference into the index's inbox**. The index is an **owned document** (public read), **materialized from its inbox** (a watcher ingests deposits → adds entries). 2. **Primary discovery = that global index.** 3. **Relational = secondary axis**, overlaid: a connection's participations, markers on the global list. Rests on existing per-item data (protected scope) — no new primitive. ## The 3-stage frame `discovery → synchronization → query` 1. **Discovery**: the index gives the NURIs of the entity documents. 2. **Synchronization**: subscribe to those documents → they **replicate locally** (verifier: `self.repos` + oxigraph dataset). 3. **Query**: query what is **now local** (sort, limit, reactivity). **SPARQL/ORM run on the local set only** (`resolve_target_for_sparql` searches `self.repos`) — you cannot query what is not loaded. **Corollary:** a reactive query does not replace the index — it runs at stage 3 on the local union that stages 1-2 built. You don't sync what you didn't discover. ## Why one reused mechanism - **No Group store.** The index is **not** open-write: it is an **owned document** (public read) **+ native inbox** (a primitive present on every document). Nobody writes the index but its owner (by materializing inbox deposits). So the model stays "3 stores + Dialog + inboxes, no Group store." - **One mechanism, reused.** The **inbox + materialization watcher** serve BOTH submitting an entity to the index AND registering to a meeting-point — same `inbox.post` API, same handling. This is exactly `inbox.ts` in this lib (`post` / `read` / `materialize` / `watch`). - **Natural dedup / moderation point:** materialization (inbox → index) is where duplicates are detected / moderated before insertion. ## Index owner — target model undecided The "dedicated service with its own wallet sharing a freely-readable index" was **incorrect**: NextGraph apps and services are **mono-user with no global data** (see [`../nextgraph-current-state.md`](../nextgraph-current-state.md) § Apps & services). The only path glimpsed for a global document is a **singleton app** bound to the developer-user — **not implemented, uncertain**, to explore later. This is why a global-index curator is a **deferred separate package** in this lib (see the top-level README). ## Polyfill reality — the fan-out drift is now RESOLVED (special-account index) The shared-wallet polyfill originally shipped a **cross-account fan-out over every account's public documents** (`store-registry.ts` `listEntityDocs('public')` / `resolveReadGraphs`) — one account saw another's public entity **without a connection**. This ADR classified that per-account fan-out as a **drift** to be replaced by the single global index. **That drift is now resolved in the polyfill.** The inbox-fed global index of this ADR is implemented on top of a **RESERVED SPECIAL ACCOUNT** in the shim (`discovery.ts`, `INDEX_ACCOUNT = "@index"`) that owns the index document while the target owner stays undecided: `submitToIndex(ref)` deposits into the index document's inbox; `readIndex()` materializes (dedup) the entries. The app-facing discovery path is now **read the index**, exactly as this ADR prescribes — NOT the fan-out. The cross-account fan-out survives only as an **internal lib fallback** (it still powers per-scope listing like `resolveReadGraphs`), never the discovery route. The special account is the provisional owner; at migration it disappears and ownership moves to the decided global-index owner (see [`../migration-guide.md`](../migration-guide.md)) with the consumer surface (`submitToIndex` / `readIndex`) unchanged. ## Alternatives rejected (mechanism) - **Open-write index** (creator writes the index directly): required a collaborative document (Group store, SDK-blocked) and exposed the index to corruption. Replaced by inbox deposit + owner materialization. - **Purely relational discovery** (`social_query`): rejected as *primary* (a global list is wanted); kept as a secondary axis. - **No index, direct reactive query**: impossible — SPARQL is local-only (stage 3).