Part of Stem. This page defines the Node blob, the single blob type that declares a resource. Everything a peer needs to know about a resource other than its content is in its Node blobs: what it is, where it sits, what its state is, and how its audience is derived.
A Node blob is a signed claim that the resource identified by space and id has kind kind, sits under parent with name, and has the state declared by target. The first Node blob of a resource has no id: its own CID is the id. The team's working name for the stable id was the "inode"; this site says node id.
Fields
The schema extends blob, so type, signer, sig and ts are present as on every signed blob.
field | type | required | meaning |
|---|---|---|---|
| literal | yes | Blob type tag. |
| no | The account whose space the node belongs to. Omitted when the signer is that account. Set by any other key, which must hold | |
| no | The id of the node this blob updates: the CID of the node's creating Node blob. Omitted on the creating blob itself, whose own CID becomes the id. | |
| yes | URL of the Kind definition the resource conforms to. Fixed for the life of the node. | |
| no | The node this one is placed under; for a top-level node, the space root's id. Exactly one parent. Omitted only on Node blobs of the space root. | |
| no | Relative name inside the parent. Omitted for unnamed nodes. | |
| yes | The state declaration: heads, snapshot, tombstone or redirect. | |
| list of CID | no | Node blobs for this same node that this one supersedes, sorted by CID. Omitted on the creating blob. |
| no | The Grant through which the signer may write this node. Omitted when the signer is the owner. | |
| no | How the node's audience is derived. Defaults to the kind's default. |
Rules
Identity. A resource is identified by its space and its node id. The node id is the CID of the resource's creating Node blob, computed with SHA-256 over the blob's DAG-CBOR bytes (see node id for why one hash is fixed). Nothing else about the node is identity: the pretty path, the name, the parent, the heads and the access mode can all change while the id stays. The resource's mutable URL is hm://<space>/<id>. An id is never reused: a tombstoned node keeps its id forever, so a later writer cannot resurrect it under old links, and there is no generation counter.
Creation. A Node blob without id creates a node. It must name a parent (the space root's id for a top-level node), and its signer must hold write or above on that parent, evaluated by the authority graph. The signer need not own the space: any key with write on the parent may create a node in another account's space, setting space to the owner and proof to the grant it relies on. The owner signs nothing for that. The creating blob's kind is the node's kind forever; a later Node blob with a different kind is rejected as invalid.
The space root. Every space has exactly one node with no parent, its root. The root's creating blob is deterministic: signed by the owner, ts 0, kind space, no id, no parent, no name, and heads naming the owner's deterministic genesis Change (the same empty ts 0 Change HM24 uses for the home document). Every device of the owner derives the same bytes and so the same root id, and any peer can compute it from the owner's key alone. Only the owner may sign Node blobs for the root. The bare URL hm://<owner> is an alias for hm://<owner>/<rootId>.
Later blobs. Every Node blob after the first carries id and must be signed by a principal with write on the node itself (which the subtree default of grants makes the same as write on any ancestor), and should list the blobs it supersedes in prev. Root update blobs carry the root id and no parent.
Forgery. An id can only be claimed by presenting the blob that hashes to it. A Node blob whose id names a creating blob the peer has not indexed is stashed until that blob arrives; the id is a dep link, so authority-first fetching requests the creating blob before anything that depends on it, exactly as a Change with a missing dep is handled today. Once the creating blob is present, authority for the update is evaluated against that node and its ancestors; authority for a creation is evaluated against the parent. Neither a sub-tree writer nor a stranger can pre-create an id, because an id is not chosen.
Concurrency and the fold. Two devices may publish Node blobs for one node without seeing each other. The peer computes the node's state by taking every authorized Node blob, dropping any blob that appears in another blob's prev ancestry, and merging what remains by target kind. All-heads heads are unioned and reduced through Change deps, which is today's multi-writer rule. A tombstone or redirect that is concurrent with a state target loses: deletion must supersede, never race. Two concurrent tombstones or redirects resolve to the lowest CID. Placement fields are read from the surviving blob, and if two survive with different placement the lowest CID wins and the client surfaces the conflict. The rule is spelled out in Resources and nodes. Timestamps play no part in it.
Targets. heads declares a Change graph; every head must share one genesis Change, and the heads are sorted. snapshot names one Snapshot blob. tombstone deletes. redirect points at another node and may republish it under this name. The kind's state field says which of heads and snapshot a kind may use; tombstone and redirect are always allowed except on the space root.
Access. inherit (the usual default) makes the node readable by its parent's readers plus the audiences of its own grants. own drops the parent's readers, which is how a private document lives under a public folder. target makes the node readable by whoever can read the node its state points at, which is how a comment follows its document. A node may only use target if its kind declares a target pointer. The computed set is defined in readers.
Proof. proof names the grant the signer relies on. The peer does not trust it; it evaluates the graph. It uses it to fetch authority first: when a Node arrives from a peer, the grant it names is requested before the Changes it points at, so the blob can be applied the moment its content lands instead of waiting in the stash.
Validation of the state. The Kind's schema types the state. For heads the schema is the document attributes schema; for snapshot it is the value schema. Validation is strict for the kinds Stem ships (space, document, comment, contact, file, schema, kind, policy) and advisory for user-defined kinds, matching how typed documents are validated today.
Today (HM24)
The Ref has three shapes: a version Ref with genesisBlob and one or more heads; a tombstone with genesisBlob and empty heads; a redirect with empty heads and a redirect target, which is also a tombstone unless republish is set. The address is space plus path, so a child Ref repeats its parent's path segments and a rename republishes every descendant. generation orders the lives of an address, chosen by the writer and never validated. visibility is a field with two values. Comments are a separate blob type identified by a TSID and carry their own visibility.
A Node keeps the Ref's job and drops the path. The three shapes become the four targets. Generation and genesisBlob disappear because an id is the CID of one creating blob and has one life. Visibility becomes grants plus the access mode. A comment is a Node like any other. Migrated HM24 documents take the SHA-256 CID of their earliest Ref blob as their node id, as described in Migration.
Examples
In the examples the root id of the Starlight space is bafyrei4lajlj6hdu6gdpmrcg4be4umr46pqm4i2hz4uep3enthjxjqi5og.
The deterministic creating blob of a space root. It has no id, no parent and no name; its CID is the root id:
{
"type": "Node",
"signer": {"/": {"bytes": "7QEE...Starlight"}},
"sig": {"/": {"bytes": "..."}},
"ts": 0,
"kind": "hm://z6MkiAKDcRSzQ4zPZfnJcS5HYx5MwgN6MU9foHihJGrhqNBj/stem/kinds/space",
"target": {"kind": "heads", "heads": [{"/": "bafyreihomegenesischange0000000000000000000000000000000000"}]}
}A new named document under the space root, signed by the owner. The blob has no id; its CID, bafyreiujzdegxdncf32epf3dhodzdocis2jhtlgmxgedn73u55xtplpft7, becomes the document's node id and its URL is hm://z6MkiAKDcRSzQ4zPZfnJcS5HYx5MwgN6MU9foHihJGrhqNBj/bafyreiujzdegxdncf32epf3dhodzdocis2jhtlgmxgedn73u55xtplpft7:
{
"type": "Node",
"signer": {"/": {"bytes": "7QEE...Starlight"}},
"sig": {"/": {"bytes": "..."}},
"ts": 1759910400000,
"kind": "hm://z6MkiAKDcRSzQ4zPZfnJcS5HYx5MwgN6MU9foHihJGrhqNBj/stem/kinds/document",
"parent": "bafyrei4lajlj6hdu6gdpmrcg4be4umr46pqm4i2hz4uep3enthjxjqi5og",
"name": "roadmap",
"target": {"kind": "heads", "heads": [{"/": "bafyreib3nq5mkq2w7e4w3slkz2n7m7xq6x4hw4x2x3vl6tz2cmkfnq5a3e"}]}
}An unnamed private document created inside the roadmap by a collaborator who holds a write grant on it. The collaborator does not own the space, so space names the owner and proof names the grant. This blob's CID, bafyreiv4seh2kvj72ceuvw75efr6edt4sywb5wkh7dnsipzz7fk4zri3r2, becomes the new node's id:
{
"type": "Node",
"signer": {"/": {"bytes": "7QEE...Collaborator"}},
"sig": {"/": {"bytes": "..."}},
"ts": 1759910460000,
"space": {"/": {"bytes": "7QEE...Starlight"}},
"kind": "hm://z6MkiAKDcRSzQ4zPZfnJcS5HYx5MwgN6MU9foHihJGrhqNBj/stem/kinds/document",
"parent": "bafyreiujzdegxdncf32epf3dhodzdocis2jhtlgmxgedn73u55xtplpft7",
"access": "own",
"proof": {"/": "bafyreigrantcollaboratorwrite000000000000000000000000000000"},
"target": {"kind": "heads", "heads": [{"/": "bafyreidraftgenesischange00000000000000000000000000000000"}]}
}A comment on the roadmap: an unnamed node in the commenter's own space, placed under the commenter's root, whose readers are the roadmap's readers. Its id is this blob's CID:
{
"type": "Node",
"signer": {"/": {"bytes": "7QEE...Commenter"}},
"sig": {"/": {"bytes": "..."}},
"ts": 1759910520000,
"kind": "hm://z6MkiAKDcRSzQ4zPZfnJcS5HYx5MwgN6MU9foHihJGrhqNBj/stem/kinds/comment",
"parent": "bafyreicommenterrootnode00000000000000000000000000000000000",
"access": "target",
"target": {"kind": "snapshot", "snapshot": {"/": "bafyreicommentsnapshot000000000000000000000000000000000000"}}
}Deleting the private document. This is an update, so it carries id (the creating blob's CID) and prev (the blobs it supersedes, here the creating blob itself):
{
"type": "Node",
"signer": {"/": {"bytes": "7QEE...Collaborator"}},
"sig": {"/": {"bytes": "..."}},
"ts": 1759996800000,
"space": {"/": {"bytes": "7QEE...Starlight"}},
"id": "bafyreiv4seh2kvj72ceuvw75efr6edt4sywb5wkh7dnsipzz7fk4zri3r2",
"kind": "hm://z6MkiAKDcRSzQ4zPZfnJcS5HYx5MwgN6MU9foHihJGrhqNBj/stem/kinds/document",
"parent": "bafyreiujzdegxdncf32epf3dhodzdocis2jhtlgmxgedn73u55xtplpft7",
"prev": [{"/": "bafyreiv4seh2kvj72ceuvw75efr6edt4sywb5wkh7dnsipzz7fk4zri3r2"}],
"proof": {"/": "bafyreigrantcollaboratorwrite000000000000000000000000000000"},
"target": {"kind": "tombstone"}
}Moving the roadmap into a plans folder is one update blob for the roadmap that changes parent and lists the previous blob in prev. The old pretty path can be kept alive by creating a new redirect node at the old place; this blob has no id because it creates that redirect node:
{
"type": "Node",
"signer": {"/": {"bytes": "7QEE...Starlight"}},
"sig": {"/": {"bytes": "..."}},
"ts": 1760083200000,
"kind": "hm://z6MkiAKDcRSzQ4zPZfnJcS5HYx5MwgN6MU9foHihJGrhqNBj/stem/kinds/document",
"parent": "bafyrei4lajlj6hdu6gdpmrcg4be4umr46pqm4i2hz4uep3enthjxjqi5og",
"name": "roadmap",
"target": {"kind": "redirect", "to": {"space": {"/": {"bytes": "7QEE...Starlight"}}, "node": "bafyreiujzdegxdncf32epf3dhodzdocis2jhtlgmxgedn73u55xtplpft7"}, "republish": true}
}The roadmap's children, comments and grants need no new blobs: they reference bafyreiujzdegxdncf32epf3dhodzdocis2jhtlgmxgedn73u55xtplpft7, which has not changed. That is the recursive move the HM24 path model could not do.
See also
Resources and nodes: the fold, versions and forgery in full.
Placement: parents, names, moves and pretty paths.
Authority: who may write a node.
Ref: the HM24 blob this replaces.
Do you like what you are reading? Subscribe to receive updates.
Unsubscribe anytime