--- type: knowledge summary: Who does what along a deposit's path — the application deposits, this package DISPATCHES on what a deposit declares itself to be and applies it arm by arm, and upstream none of it is reachable yet last_checked: 2026-08-21 --- # Who processes an inbox [[knowledge_what-an-inbox-is-upstream]] settles what the target *offers*: a document may be given an inbox, nothing above the engine exposes one, and the processor set admits nothing from outside. It says nothing about who does the work **here**. That is this leaf. The question is hard to answer out loud — *"does the polyfill process the inbox automatically?"* took one agent a long exchange to fail at — because **processing means a different thing at each layer**. Naming the actor first makes it easy. Everything below is read off the code; symbols are cited so `git grep` settles any doubt. ## The application It **deposits** by naming a document (`inbox.postToDocument`) or a person (`inbox.share`), **reads its own** with `inbox.readForDocument` / `readSyncedForDocument`, and may **watch** one with `inbox.watch`. What it never does: **process an inbox so that a `ReadCap` sent to it arrives**. `inbox.processInbox` is published, but no application has to call it — connecting drains, and every arrival afterwards is applied. It never resolves someone else's inbox address (the only published call that returns one is `storeRegistry.openDocumentInbox`, on a document it owns), and it has no way to reach, hurry or disable the deferred stand-in below. ## `@ng-eventually/polyfill` **At connection** — `emulated-verifier/connect.ts`, `connectedUser`: restore every durable register, then `processInbox` on each inbox this identity holds, then start the continuous observation. Failing to apply one queue is reported and denies nobody their session; failing to *reach* the queues rejects. **Continuously, while the identity is connected** — `emulated-verifier/inbox-observer.ts`, `startObservingInboxes`, whose only caller is `connectedUser`: one subscription per inbox, and every push runs `processInbox` on it. *Which* inboxes exist is itself subscribed to, on two channels that do not overlap: the **register** (the User branch of the private store, where `openDocumentInbox` appends its record — so an inbox opened mid-session is picked up) and **what this identity holds** (`CapRegistry.onChange` — which is what makes a person's *first* session watched at all, since provisioning is lazy and they connect owning nothing). Push-driven throughout; it never polls. It stops on identity change, on disconnection, and on `configure` / `resetConfig` (`stopObservingInboxes`, called only from `shared-wallet/bootstrap`). Every step re-checks its holder, so a run that outlives its identity abandons rather than file into the wrong ring. ### Applying is a DISPATCH — one arm per declared type `emulated-verifier/inbox-dispatch.ts`, `applyDeposit`. It is a `switch` over what a deposit DECLARES itself to be (`kindOf`), and the shape is the target's own: `Verifier::process_inbox(msg, content)` is literally `match content { … }`, one arm per `InboxMsgContent` variant, ending `_ => Err(VerifierError::NotImplemented)` (`engine/verifier/src/inbox_processor.rs`). **What an arm owes**, read off upstream's clearest arm (`InboxMsgContent::ContactDetails`): **validate** the envelope — a missing `from_inbox` is `InvalidInboxPost`, a hard failure and not a skip; **build** its addresses from the typed content; **read** the recipient's own store to see whether the thing is already there (a SPARQL `ASK`); **decide idempotently** — `ContactAlreadyExists` rather than a duplicate; and only then **write**. An arm is a real program, not a branch. The two arms this package has: - **A `ReadCap` delivery** (`inbox-dispatch.LINK_KIND`, whose name is upstream's own word for the variant) — held for the owner's ring and filed durably with `branch-registers.addLink`, the emulated `AddLink { read_cap }`. A payload declaring itself one of these and carrying nothing that reads is REFUSED and reported, which is the envelope validation above; it used to be handed back as consumer data. - **A bare `reference`** — a deposit whose payload IS a `did:ng:` reference and nothing else: no operation, no claim, no copy of any value. `emulated-verifier/index-deposit.ts`, `applyIndexDeposit`, resolves it and LOOKS: which document the inbox belongs to (the owner's own `AddInboxCap` pairing, `readInboxCapPairs`), what that document declares it indexes by (`urn:ng-helpers:index:field`, on the document's own NURI as subject), whether the reference is already in, what the referenced object says for that field — and only then writes ONE entry (`urn:ng-helpers:index:value`, subject = the object's NURI). Since anyone may deposit, a payload carrying an operation would be a licence to rewrite someone else's document, and a payload carrying a value would put in the index something the object does not say. **An index only ever grows**: nothing there removes an entry and nothing there can express a removal, which is what makes an empty read harmless — absent, unreadable and failed all mean *not added this time*, and a later deposit adds it. An unresolved reference is reported and costs nothing; an already-indexed object is passed over; a document declaring no field, or several, refuses rather than write something wrong — and that refusal leaves READING its entries untouched. *(Those two IRIs are `@ng-helpers/indexing`'s namespace, kept letter for letter because renaming them is a data-format change nobody decided. This package writing another package's namespace is odd, and it is recorded as odd rather than acted on.)* **A kind with no arm says so.** A payload declaring itself in this package's reserved `urn:ng-eventually:inbox:` namespace for a variant nothing here applies is REPORTED — upstream's `NotImplemented`, which is an answer. Until 2026-08-21 it was read, counted, and produced nothing at all: no effect and no reported failure, the "failure disguised as an absence" this package has closed repeatedly elsewhere. **Consumer data is not a variant.** A payload that declares no kind of this package's is claimed by no arm and nothing is reported about it: it is not an unapplied message, it is not a message. That is why the RESERVED NAMESPACE is the discriminator and the mere presence of a `kind` field is not — `{ kind: "join" }` is a consumer's word for a consumer's message, and it stays one. **What still reaches the caller.** Only a `ReadCap` is withheld: an application must never handle one. Everything else `read` found is returned unchanged, including the payloads an arm applies — a `reference` is public information, so withholding it would buy nothing. Two senses of *applied* stay apart: `read` learns a `ReadCap` **in memory, for this session**; only `processInbox` makes it **durable** and runs the dispatch. `inbox.watch` re-reads on every push, so it absorbs and files nothing. **After a deposit whose recipient is absent.** A deposit that landed arms a deferred stand-in for the session the owner does not have. What it achieves is the only part written down outside the package: **deposits converge** — a share becomes readable without the recipient re-opening the page. *How* is deliberately not documented here. The mechanism is unpublished by design: nothing lets a caller ask for it, name another user's inbox, or turn it off, and the confidentiality of the whole shared-wallet emulation rests on its being unreachable. Its limit is stated rather than hidden — it lives in a page, so if the page goes away the deposit waits for its owner's next connection, which is the real path and not a repair. **And it applies the registers, not the documents.** Running for an owner who is not the one holding the page, it takes the arms that write the verifier's own bookkeeping and stops there: writing application data into an absent owner's documents from a stranger's session is a different act, and this package does not take it. So a `reference` deposit is left — traced, not reported, because waiting is not failing — and the owner's own next connection applies it. Nothing is lost: see below. **Nothing is ever consumed.** No path retires an applied deposit: an inbox here is re-readable where upstream it is a queue. That is what makes every abandon, every reported refusal and every watch that could not be opened harmless — what was not applied is still there for the next push or the next connection. It is also what makes re-applying safe: every arm is idempotent, so seeing every deposit again on every run lands on the same result. ## NextGraph itself The **broker** routes a sealed message by inbox key (`inboxes: PubKey → RepoId`, `engine/verifier/src/verifier.rs`) and queues it while the recipient is away. The **recipient's own verifier** applies it inline as it arrives — `LocalBrokerMessage::Inbox` → `session.verifier.inbox(&msg, from_queue)` in `sdk/rust/src/local_broker.rs`; `from_queue` distinguishes the backlog handed over at connection from the messages that follow, and **both go through the same door**, which is why the continuous regime above is the rule and the connection drain the special case. And **none of it is reachable from JS**: the processor set is closed, the variants that would carry a payload carry nothing, and the post command has no handler arm — all in [[knowledge_what-an-inbox-is-upstream]]. Every actor above except the broker and that verifier is therefore ours, emulating one thing: a session applying its own inbox, continuously.