Worked cases
Eight concrete sync situations worked through in Stem terms, naming the Node and Grant blobs involved, the readers of each node, the scope sets exchanged, the RPCs that run, what the ledgers record and what each peer ends up holding, with a conclusion on flat sets versus nested trees.

Part of Stem. This page is the exercise the Seed Resources board asked for: take concrete cases and write down exactly what exists and what moves. Each case names the blobs, the readers of each node, the scope sets exchanged, the RPCs that run, and what lands in the disclosure and transfer ledgers.

Notation. A, B are spaces and also their owner accounts. A.site is A's declared site peer. A.d1, A.d2 are A's devices. N(A, x) is the node with id x in space A, G(subject → audience, level) a Grant, readers(N) the computed reader set. Every space root carries the owner's implicit permanent admin.

Case 1: public multi-space sync

Peers P1 and P2 each follow spaces A and B. Everything is public.

    Blobs. For each space: the root Node, one G(root → everyone, read), and for every document a Node with target.heads plus its Changes and files. No other authority blobs.

    Readers. Every node inherits from root, so readers = everyone throughout.

    Policy. Both peers hold follow on {A, root, subtree} and {B, root, subtree}.

    Exchange. P1 runs one sync run per space against A's and B's authority peers (owners' devices, sites) and against P2 as a trusted peer if either has joined the other. Per space: Reconcile until agreed, Fetch of the lacking CIDs, Watch while connected. The scope set is the whole space, unfiltered, because nothing is hidden.

    Ledgers. Every served blob is a disclosure with basis public. One transfer per peer per run.

    End state. Both hold everything for A and B. One flat set per scope is enough; the case does not distinguish flat sets from trees.

Case 2: a private space

Space S has owner O and writers W1 and W2 on their own devices. Peer X is a public site that is not S's site.

    Blobs. Root Node of S. G(S.root → key W1, write), G(S.root → key W2, write), both signed by O. No grant to everyone. Documents under root with access = inherit.

    Readers. readers(S.root) = {O, W1, W2}, and every inheriting node has the same set.

    Exchange. O, W1 and W2 authenticate to each other and to S's site if one is declared. Reconcile on {S, root, subtree} runs over the full set because every participant is a reader. X, authenticating as nobody relevant, computes a scope set of nothing for S: ListSpaces from X's side does not list S, Reconcile returns ranges over the empty set, Fetch lists every CID as missing.

    Ledgers. Disclosures among O, W1, W2 with basis grant, naming the recipient account and the grant. X appears nowhere.

    End state. O, W1, W2 hold S. X holds nothing of S and cannot tell S exists on those peers. The storage-versus-access split the board wanted (X holding S's bytes without reading them) is exactly the sync level and is not available until encryption; the case records that honestly.

Case 3: an externally shared document

Document D in private space S is shared with account E, who is not a member of S.

    Blobs. Everything from case 2, plus one G(N(S, D) → key E, read) signed by O (or by W1, whose write lets it issue read on D). No change to D's Node.

    Readers. readers(N(S, D)) = {O, W1, W2, E}. Children of D with access = inherit get the same set, because inheritance adds the parent's readers; E can therefore read D's subtree and nothing else in S. Comments on D have access = target, so their readers are D's readers too, and E sees the discussion.

    Exchange. E's device authenticates to S's authority peers and syncs the scope {S, D, subtree}. The scope set computed for E is D's Node, Changes and files, D's children, comments on them, and the one Grant, filtered to what E may read: an embed in D of another S document that E cannot read is a embed link whose target is simply missing for E. Nothing about the rest of S enters a fingerprint E sees.

    Ledgers. Disclosures to E with basis grant naming the share grant. If E later publishes a comment, E's daemon Publishes it and Offers it to S's authority peers under scope {S, D, exact, comments}; they accept because E is the comment's author and D's reader, and record a transfer from E.

    End state. E holds D's subtree and its discussion. The rest of S never reached E. The "deciding case" from the earlier board worksheet turns out not to require re-homing anything: readers are computed per node, and the scope set is filtered per reader, so a narrower share is one Grant blob.

Case 4: a shared immutable schema

Schema resource Σ in space A is referenced by typed documents in spaces B and C at a pinned version.

    Blobs. N(A, Σ) with target.snapshot pointing at a Snapshot whose value is a schema; G(A.root → everyone, read). Documents in B and C carry attributesSchema: hm://A/Σ?v=<cid>.

    Readers. everyone.

    Exchange. A peer following B does not follow A. When it indexes a B document, the handler emits a schema link to Σ at the pinned version; the retention rules say schema is a presentation dependency fetched for validation. The peer issues Sync for scope {A, Σ, exact, state} with version set; the run contacts A's authority peers, reconciles the tiny scope and fetches the Snapshot. It stops at the pinned version even if Σ has moved on.

    Ledgers. Disclosures with basis public from A's peers.

    End state. The B follower holds Σ at the pinned version and nothing else from A. This is link-driven fetching, and it works with flat sets.

Case 5: writer revocation

Owner O revokes writer W of space S at time t.

    Blobs. R(G(S.root → key W, write)) signed by O. W's Node and Change blobs signed before and after t.

    Readers and authority. On the live pass of the authority graph, W drops out of readers(S.root) and loses write. Every node W wrote is re-evaluated: Node blobs whose signer is W and whose authority came only through the revoked grant are no longer heads. Their state is kept but unapplied, which is the "mistaken revocation can be undone" lean: a new grant to W makes them heads again.

    Exchange. Nothing new is exchanged except the Revocation itself, which travels in the authority facet to every reader. W's device, on its next Authenticate + Reconcile, computes a scope set of nothing for S. Any peer still watching S sends W no notifications.

    Ledgers. ListDisclosures filtered to W's account shows exactly which blobs W was served before t. That is the honest answer to "what does W still have".

    End state. Members hold S with W's post-revocation blobs stored but not applied. W holds what W had. Flat sets suffice; the set W is allowed to see simply becomes empty.

Case 6: a comment on a private document

E (from case 3) comments on D.

    Blobs. N(E, c1) in E's own space, unnamed, kind comment, access = target, target.snapshot pointing at a Snapshot whose value has target = N(S, D) and a body.

    Readers. readers(N(E, c1)) = readers(N(S, D)) = {O, W1, W2, E}. E's own root being public does not make the comment public.

    Exchange. E's daemon offers the comment to S's authority peers under {S, D, exact, comments} and to E's own authority peers (E's devices) under {E, c1, exact}. S's peers accept on the author basis and index it into D's comments facet. O's device, watching D, receives a notification and fetches the comment.

    Ledgers. Transfers from E recorded at S's peers; disclosures to O, W1, W2 with basis grant naming D's share or membership grants.

    End state. A reader of D sees the comment; a reader of E's public space does not, because the comment is not in any scope set computed for everyone.

Case 7: moving a subtree with an outsider's grant on one child

In space S, folder F contains documents D1 and D2. Account E holds G(N(S, D2) → key E, read). O moves F under another folder H.

    Blobs. One new Node blob for F with parent = H, prev = [previous F Node]. No blob for D1 or D2 changes: their parent is still F's id. Optionally a redirect Node at F's old name under its old parent.

    Readers. Recomputed from placement: D1 and D2 inherit from F, F from H. If H has the same readers as F's old parent, nothing changes; if H is narrower, D1 and D2 narrow with it. E's grant is on D2's node, so E keeps read on D2 wherever D2 is placed. The authority graph did not change at all; only the placement graph did.

    Exchange. The scope {S, F, subtree} has one new blob, the Node for F. Peers following S fetch it. E, syncing {S, D2, subtree}, sees no change in that scope set because D2's blobs are unchanged; E's next Reconcile agrees in one round.

    Ledgers. One disclosure of the new F Node per reader.

    End state. A recursive move is one Node blob, comments and children travel because they reference ids, and an outsider's access is untouched. This is the inode result.

Case 8: a device back online with a stale policy

Account A has devices d1 and d2. While d2 was offline, A on d1 changed the policy: stopped following space B, started pinning space C.

    Blobs. A new Snapshot of N(A, policy) with prev set to the old one, signed by A on d1, plus the policy Node blob. readers(N(A, policy)) = {A} because it has access = own and one G(policy → key A, read).

    Exchange. d2 comes online and syncs its own space first (pin on {A, root, subtree} is a default rule). The scope set includes the policy node's blobs because d2 authenticates as A. d2 fetches the new Snapshot, reloads the policy, and from then on does not reconcile B and begins pinning C. Any offer for B that arrives in the meantime is answered with not-wanted, and B's blobs already held become collectable.

    Ledgers. Disclosures from d1 (or A's site) to d2 with basis grant naming the policy grant.

    End state. Both devices run the same rules without any device-to-device settings channel. Open: what d2 should do with blobs it fetched for B between coming online and reloading the policy; the lean is to keep them until the next collection pass.

What the cases show


    Cases 1, 4 and 6 need nothing beyond a flat reconciled set per scope, filtered per reader.

    Cases 2, 3 and 5 are decided by per-node readers and the per-connection filter, not by moving rows between partitions. Sharing one document with an outsider is one Grant and leaves the rest of the space untouched; revocation makes a reader's filtered set empty.

    Case 7 is the payoff of stable node ids: a move is one blob and authority does not move.

    Case 8 shows the policy as a resource doing what a settings table could not.

None of the eight cases requires a nested, partitioned tree. What a tree would buy is cheaper fingerprints for very large spaces with many distinct reader sets, where today's filtered fold must walk the hidden items on every round; that is a performance question, recorded in Open questions, not a correctness one.

Do you like what you are reading? Subscribe to receive updates.

Unsubscribe anytime