The protocol in one page
The whole Stem model in one flow, the six decisions that define it, and one worked example that follows a private document shared with an outsider from the signing key to the disclosure ledger.

Part of Stem. This page is the shortest complete account of the protocol. It states the six decisions that every other page elaborates, shows how a blob travels from a signing key to a reader on another peer, and works one example end to end.

The flow

flowchart LR K[Account key] -->|signs| B[Blobs: Node, Change, Snapshot, Grant, Revocation, Group] B -->|Publish| H[Handler] H --> F[Facts] H --> L[Links] H --> A[Authority graph] A --> R[readers per node] R --> X[blob access] X --> SS[Scope set per requester] SS -->|Reconcile, Fetch, Offer, Watch| P[Other peers] P -->|Transfer| H2[Their handler] X -->|every serve| DL[Disclosure ledger] P -->|every receipt| TL[Transfer log]

An account key signs blobs. Every blob enters a peer through one handler, whether it was published locally or received from another peer. The handler reads the blob's kind definition, validates it, and emits facts and links. Grants and revocations feed the authority graph, which yields the readers of every node and so the access table for every blob. Sync is a conversation about a scope; the scope set a peer discusses is already filtered to what the other side may read. Everything served is written to the disclosure ledger, and everything received is written to the transfer log.

The six decisions

One blob declares a resource. A Node blob carries identity (a space and a node id), the kind the resource conforms to, placement (a parent node and an optional name), a state target that is one of Change heads, a Snapshot, a tombstone or a redirect, a prev list of the Node blobs it supersedes, and an access mode. The node id is the CID of the Node blob that created the node: the creating blob carries no id, and every later Node blob for the node carries that CID, exactly as a Change carries its genesis. A resource's mutable URL is therefore hm://<owner>/<nodeId>. Any key holding write on the parent may create a node in another owner's space; the owner signs nothing. The Node replaces HM24's Ref in all three of its shapes. Ordering between Node blobs of one node is by prev, never by timestamp, and because ids are CIDs they are never reused and there are no generations.

Two state representations. A resource's state is either a graph of Change blobs, exactly the CRDT documents use today, or a chain of Snapshot blobs, each holding the complete value. Comments, contacts, schemas, kinds, files and sync policies are Snapshot resources. A kind descriptor says which representation its resources use and which schema the state must satisfy.

Authority is a graph of grants. A Grant says that an audience holds an access level over a subject. Holders delegate at their level or lower. A Revocation cuts an edge. Groups are permanodes whose members are grant holders. Evaluation is late-bound and order-independent: peers holding the same grants and revocations compute the same result. Public content is a grant to the audience everyone.

Audience is computed and materialised. For every node the daemon computes its readers: the parent's readers when the node inherits, the audiences of live grants covering the node, and always the owner and admins. Every blob is mapped to the nodes whose state it belongs to, so "may this peer read this blob" is a join against the requester's identity. Forbidden and absent are indistinguishable on every surface.

Privacy bookkeeping is two ledgers. Every blob served to a peer is recorded with its basis. Every batch received is recorded with the scope the sender claimed and what became of each blob. Revocation is therefore honest: it stops future serves, and the ledger says who already holds the bytes.

Sync is scoped, policy-driven and authority-first. A peer syncs scopes, never "everything peer X has". Standing interest is a policy resource with rules to follow, pin, fetch on demand or ignore. Peers reconcile a scope set with range-based set reconciliation, then Fetch what they lack with groups and grants first, then nodes, then state, then files. A peer offers blobs for a claimed scope with proof instead of pushing arbitrary CIDs, and watches a scope for live updates instead of polling. The peers a daemon talks to are the space's authority peers and the account's trusted peers, not a random sample.

A worked example

Alice owns space A. Her laptop runs a daemon; so does the site that publishes A. Alice writes a private design document and shares it with Bob, who is not a member of A. Here is every step in Stem terms.

    Creating the document. Alice's app signs a genesis Change and a Node blob with no id (a creating blob: its own CID, bafyreib7x…, becomes the node id), kind document, parent the root id of A, no name (the document is private and has no pretty path yet), target heads naming the Change, and access own. Because Alice is the owner of A she needs no proof. Her daemon's handler validates both blobs, records the node, and computes its readers: with access own and no grants yet, the readers are Alice alone (the owner is always a reader) and any admins of A.

    Sharing with Bob. Alice's app signs a Grant with subject {kind: node, space: A, node: bafyreib7x…}, audience {kind: key, key: <Bob>}, access read. The handler adds the edge to the authority graph and recomputes readers of the node: Alice and Bob. The Node blob and the Change already map to this node in the blob access table, so Bob may now read all three blobs, and nothing else in A.

    Reaching the site. Alice's policy follows A, and A's declared site holds a sync grant on the space root, so the site is an authority peer. Alice's daemon calls Offer on the site with scope {space: A, node: bafyreib7x…, depth: exact} and the three CIDs. The site's policy follows A as a whole, so it accepts the scope, finds it lacks all three, and fetches them from Alice. Each blob validates into the claimed scope: the Node's signer is the owner, the Change is reachable from the Node's heads, the Grant's subject is the node. The site records one transfer with three indexed blobs. Its handler computes the same readers Alice's did.

    Bob reads. Bob's app opens the link Alice sent, hm://A/bafyreib7x…, the document's mutable URL. Bob's daemon calls Sync for that scope. The space's authority peers include the site, so it connects, authenticates as Bob with Authenticate, and calls Reconcile. The site derives the scope set for the node and filters it to blobs whose nodes have Bob among their readers: the Node, the Change and the Grant. Bob has none, so the fingerprints differ and the site lists the three CIDs. Bob's daemon calls Fetch; the site returns the Grant first, then the Node, then the Change, and writes three disclosure rows, each with basis {kind: grant, account: <Bob>, grant: <the Grant's CID>}.

    An outsider tries. Carol, who has no grant, asks the site for the same scope. The site's scope set for Carol is empty; its reply is identical to the reply for a node that does not exist. She learns nothing, not even that the node exists.

    Revocation. Alice signs a Revocation naming the Grant. The authority graph is re-evaluated, Bob leaves the readers of the node, and from now on the site serves him nothing for it. Alice's app can call ListDisclosures on the site and see that Bob received three blobs on the day she shared them. Nothing is deleted; what Bob already holds, he holds.

    Moving the document. Later Alice gives the document a name and places it under a folder node. She signs one new Node blob with id set to bafyreib7x…, parent set to the folder, name set to design, and prev naming the previous Node blob. The Grant still names the node id, Bob's access is unchanged, the comment Bob left still targets the node id, and the pretty URL hm://A/projects/design now resolves to it. No child, comment or grant had to be rewritten.

That example touches every part of the specification: Node and Change for the data, Grant and readers for access, scope set and the sync RPCs for transport, disclosure and transfer for bookkeeping, and placement for the move.

What is unchanged

Signing, encoding and content addressing are exactly Signed blobs. Document content is exactly the Change CRDT over blocks. Files are UnixFS. Accounts are keys. The hm:// URL grammar gains one form, a node id as the first path segment, and one query parameter, n. The libp2p transport and range-based set reconciliation are as described in Network; what changes is what travels over them.

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

Unsubscribe anytime