Resources and nodes
What a resource is in Stem, how its identity is a space and a node id, how its state is folded from Node blobs by causal order rather than timestamps, why generations are gone, how forgery is prevented, and how the space root, documents and comments all fit one rule.

Part of Stem. This page defines what a resource is, how it comes into being, how its current state is computed from the blobs that declare it, and how concurrent declarations are merged. It replaces the earlier "Named roots" page, whose tuple is now the Node blob.

Identity

A resource is identified by a space and a node id, and by nothing else. The space is an account's public key. The node id is the CID of the Node blob that created the resource, so it is derived, unforgeable and never reused. The pair is written hm://<space>/<node-id>, and that is the resource's mutable URL: it keeps resolving however the resource is renamed, moved or edited. Everything else about a resource can change: its name, its parent, its state, its audience, even whether it exists. Its identity cannot.

The words "resource" and "node" name the same thing from two sides. From the blob side it is a node: an entry in the space's placement tree, declared by Node blobs. From the state side it is a resource: the document, comment, contact or schema a reader sees. The official vocabulary decided in August 2026 is that mutable things are resources, and this site follows it; "node" appears when the discussion is about the blob or the tree. The team sometimes called the stable id an "inode". This site says node id.

Today's protocol identifies a document by its path, and by the genesis Change behind the path. A path is a name and a location at once, so renaming is identity loss unless a redirect is left behind, and a child's Ref repeats its parent's path segments. The roadmap records the decision to separate identity from placement with stable node ids. Stem is that decision written down as data.

Node ids

A node id is the CID of the node's creating Node blob, written as CIDv1, dag-cbor, SHA-256, base32 lowercase: 59 characters starting bafyrei. The storing peer may hash its blob table with BLAKE2b as today; the id is always the SHA-256 form, so every peer derives the same id from the same bytes. There are no client-chosen ids and no reserved id strings.

The rule mirrors the Change graph exactly. A genesis Change carries no genesis and its CID becomes the document's identity; every later Change names it. A creating Node blob carries no id and its CID becomes the node's identity; every later Node blob for the node names it in id. Because the id is a CID, the creating blob is always fetchable, and the handler treats id as a dep link so the creating blob arrives before any update to it.

The space root has an id too, derived rather than reserved: its creating blob is deterministic (see below), so anyone who knows the owner's key can compute the root id. The bare URL hm://<space> is an alias for hm://<space>/<root id>.

Creation

A node exists once its creating Node blob, a Node blob with no id, has been indexed and authorized. "Authorized" means the signer holds write on the parent node named by the blob, or is the space owner. Any key with write on the parent may create nodes in another account's space; it sets space to the owner, and the owner signs nothing. For the space root only the owner's deterministic creating blob qualifies. The creating blob fixes two things for the life of the node: the id, which is its own CID, and the kind. A later Node blob for the same id that names a different kind is rejected and not stashed, because a kind change is not a conflict to be merged but a forgery or a bug.

The creating blob has no prev and no id. It may carry any target: a document usually starts with heads naming its genesis Change, a comment starts with snapshot, and a node may even be created as a redirect when a writer wants a name to point somewhere from the start.

State

The state of a resource is computed from the set of its authorized Node blobs by causal order, then by a deterministic merge rule. The procedure, called the fold, is:

    Collect the creating Node blob and every indexed Node blob whose id is its CID, keeping those whose signer, at the time of evaluation, holds write on the node (or on the parent, for the creating blob), and whose kind equals the creating blob's kind.

    Remove every blob that appears in the prev list of another collected blob, transitively. What remains are the node's head declarations. In the common case there is exactly one.

    If there are several heads, merge them by the kind of their targets.

The merge rules are fixed and deterministic, so every peer that holds the same blobs computes the same state.

Concurrent heads

Result

heads and heads

The union of the Change heads, minus any Change that another applied Change depends on. This is exactly today's head rule; two writers who publish at once produce a two-head version and the next Change merges them.

snapshot and snapshot

Both Snapshots are heads. The client shows a conflict and the next Snapshot lists both in its prev, which resolves it. A kind's schema may define an automatic merge for its value; none of the core kinds does.

heads and snapshot

The Snapshot is treated as one more head of the Change graph. A Change may name a Snapshot in its deps, so the next edit merges them.

a state target and a tombstone or redirect

The state target wins. A deletion or a move must supersede the state it replaces by listing it in prev; it never wins a race. This is what makes a mistaken deletion recoverable and a lost move visible.

tombstone and tombstone, or redirect and redirect

The head with the lowest CID wins. Deterministic and arbitrary, because the two agree on what matters.

Placement (parent, name, access) is read from the single head when there is one. With several heads whose placement differs, the head with the lowest CID supplies the placement, and the client surfaces the disagreement so a writer can publish a merging Node blob.

The fold never consults timestamps. The ts field on a Node blob is advisory, shown to people and used to order lists, and it plays no part in which declaration wins. This answers the earlier open question "which root dominates": the one that is not superseded, and when two are not, the rule of the target kind.

A worked example

Alice edits her document on two laptops that were both offline. The document is node bafyreib7x… in her space; its creating Node blob was N0, which is also its single head, with target.heads = [C5].

    Laptop A publishes Change C6 (deps [C5]) and Node blob N1 with heads = [C6], prev = [N0].

    Laptop B publishes Change C7 (deps [C5]) and Node blob N2 with heads = [C7], prev = [N0].

A peer that holds all of this folds the node: N0 is in the prev of both N1 and N2, so it is removed; N1 and N2 remain as two heads; both targets are heads, so the Change heads are unioned to [C6, C7]. The version is C6.C7. Nobody had to do anything.

Later, laptop A comes online, sees both heads, and Alice makes one more edit. Laptop A publishes Change C8 with deps [C6, C7] and Node blob N3 with heads = [C8], prev = [N1, N2]. The fold now yields the single head N3 and the version C8.

If instead laptop B had deleted the document while A was editing, B's tombstone N2' with prev = [N0] would be concurrent with N1. The state target wins: the document is alive at C6, and the client shows Alice that a deletion was attempted. To delete it now she publishes a tombstone with prev = [N1, N2'].

Versions

A version is an immutable reference to one state. For a Change graph it is the sorted head CIDs joined with .; for a Snapshot it is the Snapshot CID. This is the same ?v= value as today's URLs. A version never names a Node blob, because the same state can be declared by many Node blobs over the node's life; it names the content.

A reader that holds a version can verify it without trusting any peer: the signatures and hashes of the Change graph or Snapshot are enough. What the Node blob adds is the claim that this state is current for this identity, and that claim is what the fold evaluates.

Why generations are gone

Today a document that is deleted and re-created at the same path gets a new generation number, and the highest generation wins, because the path was reused. The generation is chosen by the writer and never validated, and the genesisBlob on the Ref exists to tell lives of a path apart.

In Stem a node id is never reused. A tombstoned node stays tombstoned under that id forever; its history stays retrievable by version. A new document at the same name is a new node with a new creating blob and therefore a new id. The name is free to reuse; the identity is not. So there is nothing for a generation to order, and nothing for a genesis blob on the Node to disambiguate. The document's own genesis Change still exists and still gives the Change graph its identity, and every head in target.heads must share it. The Node blob simply does not need to repeat it.

Forgery

Forgery is a writer claiming an id they have no authority over, by placing a Node blob for it where they can write. Two rules prevent it, and the derivation of ids removes the third case HM26 worried about.

    Existing ids are protected by their own authority. A Node blob that carries an id is accepted only if the creating blob with that CID exists and its signer holds write on that node. Where the blob says the node is placed does not matter; the authority graph is consulted for the node, not for the claimed parent. Because id is a dep link, a peer fetches the creating blob first and never has to guess whether an id is new.

    New nodes are protected by the parent. A Node blob without an id creates a node, and is accepted only if its signer holds write on the parent it names. The id it receives is its own CID, which nobody chose and nobody can collide with.

There is no third rule for migrated or reserved ids: a migrated HM24 document's id is the CID of its earliest Ref blob, derived by the migration box, and it is protected by rule 1 like any other id.

The roadmap's phrase is that the authority graph is critical and the placement graph is for naming. The fold follows it: who may say what about a node is decided by grants on the node and its ancestors, as described in Authority. A Node blob whose signer is not yet authorized is stashed, not dropped, and re-evaluated when a grant arrives.

The space root

The space root is the space itself. It is of kind space: its state is a Change graph whose attributes carry what today's Profile blob and home document metadata carry (name, icon, description, alias, site), and whose blocks are the home document. Only the owner may publish Node blobs for it, it has no parent and no name, and it cannot be tombstoned or redirected. Its creating blob is deterministic: signed by the owner with ts 0, kind space, no name, no parent, no id, and heads naming the owner's deterministic genesis Change (the same empty Change HM24 uses for the home document). Ed25519 signatures are deterministic, so every device derives identical bytes and the same root id. This is the roadmap's "home documents become profiles" decision: one owner-only node at a fixed, derivable id under an empty parent.

Because the root is a node like any other, grants on it cover the whole space by default, and every other node inherits its readers from the root unless a node breaks inheritance. "Members of a space" is simply the readers of its root.

Comments as resources

A comment is a node of kind comment in its author's space. Its state is a Snapshot whose value names the target document, the thread root and reply parent, and the blocks. Its id is the CID of its first blob, as for every node; the TSID HM24 used stays resolvable through the migration. It has a parent (the author's space root) but no name, so it does not appear in anyone's directory, and its access mode is target, so its readers are the readers of the document it comments on.

This resolves the question of whether comments are resources or triples. They are resources, because they have identity, a lifetime, edits and deletion, and because the fold and the authority graph treat them like everything else. They are also triple-like, because they are unnamed, they carry no children, and their audience is not their own but their target's. The Kind descriptor expresses both facts: naming: unnamed, children: false, access: target. Nothing about comments is special-cased in the daemon.

Editing a comment publishes a new Snapshot and a new Node blob with prev; deleting it publishes a tombstone Node blob. An edit that arrives before its predecessor is stashed until the predecessor is indexed, which replaces today's rule that the live comment is the one with the highest timestamp.

Today (HM24)

Today

Stem

Identity is the path, and the genesis Change behind it

Identity is the space plus node id; the genesis Change identifies the Change graph only

A Ref declares heads, a tombstone, or a redirect, keyed by path

A Node blob declares heads, snapshot, tombstone or redirect, keyed by node id

Latest Ref per signer by timestamp; heads unioned across signers

Heads are the Node blobs not superseded through prev; merged by target kind; timestamps advisory

Generations order the lives of a path; writer-chosen, unvalidated

Ids are never reused; no generations

Home document and Profile are separate records

One space root node of kind space, with a derivable id

Comment identity is author plus TSID; live version by timestamp

Comment is a node whose id is the CID of its first blob; live state by prev

Deletion is the newest Ref having no heads, by timestamp

Deletion is a tombstone that lists what it supersedes; a racing tombstone loses

The shipping behaviour is documented in Documents and Comments.

Open questions

    Open: should a kind be allowed to declare an automatic merge for concurrent Snapshots, and in what language? The core kinds do not need it.

    Open: whether the client should require a writer to acknowledge concurrent placement before publishing a merging Node blob, or merge silently with the lowest-CID rule.

See also

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

Unsubscribe anytime