Part of Stem. This page defines placement: the parent and name a node has, the tree they form, and everything that depends on it: paths, moves, redirects and URL resolution. Placement is for naming and addressing. It carries no identity and no authority of its own.
Parent and name
Every node except the root has exactly one parent, and at most one name inside that parent. Both are fields of the node's head Node blob. The parent is a node id in the same space. The name is one path segment: no slash, no control characters, at most 255 characters.
A node without a name is an unnamed node. Comments are unnamed by their kind. A private document may be unnamed so that its existence is not visible in a listing and its URL carries no human-readable hint. An unnamed node is still placed: it has a parent, it inherits the parent's readers unless it breaks inheritance, and it moves with the parent.
There are no hard links. A node is both a directory and a file: it can hold state and it can hold children, so there is no need for a second kind of entry that points at a node from elsewhere. If two places should show the same content, one of them is a node whose target is a redirect to the other.
The parent must exist and be of a kind that allows children. The space root allows children; comments do not. A Node blob naming a nonexistent parent is stashed until the parent arrives.
Name resolution
Names are chosen by writers, and two writers may choose the same name under the same parent, or one writer may do so by mistake on two devices. Identity is unaffected: both nodes exist and are reachable by id. Resolution decides which one a pretty path shows.
Among the nodes that currently claim a name under a parent, the one whose head Node blob was signed by the principal with the highest access level over the parent wins; among equals, the lowest CID wins. So the space owner's naming beats a writer's, an admin's beats a writer's, and two writers' collide deterministically. The losing node remains fully functional under its id, and a listing shows both with the collision marked, so a writer can rename one.
A tombstoned or redirected node does not hold its name against a live node: a live node with the same name under the same parent wins over it regardless of signer. This is what lets a name be reused after a deletion without reusing the id.
Paths
A path is the chain of names from the root to a node, joined with slashes. It is derived from placement by walking the tree top down, and it is never stored in a blob. A node with an unnamed ancestor has no path. A node whose name lost a collision has no path either, until the collision is resolved.
Today a path is the identity, the hierarchy and the permission scope of a document, and a child's Ref repeats every segment of its parent's path. The team called renaming under that model unreliable: every descendant needs a new Ref and a redirect, and the app had to build a whole project, Reliable document references, to keep parent cards consistent across renames and moves, with rabbit holes such as resuming a partially completed recursive move. In Stem a path is a view over placement. Nothing has to be kept consistent, because nothing is duplicated.
Pretty paths are the space owner's responsibility, in the roadmap's words a liability they take on. Nothing in the protocol requires a node to have one, and a space may be organised entirely by ids with names used only for the pages its owner wants to be reachable by a readable URL.
Moves
A move is one new Node blob. It names the same id, a new parent or name or both, the same target as before, and the current head in prev. The signer must hold write on the node, on the old parent and on the new parent, which is the roadmap's rule that creating a name in a parent pointing at a node needs authority over both.
Nothing else changes, because nothing else refers to the node by its placement.
Children reference the moved node by its id as their parent. They move with it, however deep the subtree, at no cost. A recursive move is a single blob.
Comments reference the moved node by its id in their target. They stay attached.
Grants whose subject is the moved node or one of its ancestors keep applying exactly as before, because a subject is a node id. A move can change which grants cover a node only by changing its ancestors: a node moved out of a shared folder into a private one stops inheriting the shared folder's readers. That is the intended meaning of moving it there, and the client should say so before publishing.
Links written with full context (node id plus path) keep resolving through the id. Links written with a path only resolve through the redirect, if one is left.
The move takes effect everywhere the Node blob arrives and is folded. There is no partial state: a peer either has the new head and sees the subtree in the new place, or has the old head and sees it in the old place.
Leaving a redirect
When the old name should keep working for readers who hold a path-only link, the mover publishes a second Node blob: a creating blob for a new node, placed at the old parent under the old name, whose target is redirect to the moved node. This is optional. Readers who arrive at the old path are sent to the new one; a listing shows the redirect as a pointer. Setting republish on the redirect makes the old name keep showing the moved node's state in place, which is what today's republish Ref does.
Moving across spaces
Identity cannot cross spaces: a node id lives in one space. Moving a document from Alice's space to Bob's is a copy and a redirect: Bob (or a writer of Bob's space) creates a new node whose target names the same Change heads, so the content and its history travel intact, and Alice publishes a redirect at the old node. Comments on the old node stay attached to the old node and are reached through the redirect; new comments target the new node. The two nodes are different resources with a shared history, which is the honest description of what happened.
A worked example
Alice's space has this tree. Ids are CIDs, shortened here to labels.
space root (id r0, the deterministic root id)
└── docs (id d1)
├── guide (id g1)
│ ├── install (id i1)
│ └── usage (id u1)
└── notes (id n1)The pretty path of usage is /docs/guide/usage. Bob, a writer of the space, has commented on usage; a grant gives Carol read on guide and its subtree.
Alice renames guide to handbook and moves it under notes. She publishes one Node blob for g1: parent = n1, name = handbook, the same target, prev = [previous head of g1]. The tree is now:
space root (r0)
└── docs (d1)
└── notes (n1)
└── handbook (g1)
├── install (i1)
└── usage (u1)The pretty path of usage is /docs/notes/handbook/usage. i1 and u1 published nothing. Bob's comment still targets u1. Carol still reads g1 and its subtree, because her grant's subject is g1. A link someone wrote as hm://alice/docs/guide/usage?n=u1 resolves through n=u1 and lands on the moved page; the client may note that the path in the link is stale.
Alice also wants /docs/guide to keep working for readers of an old newsletter. She publishes a creating Node blob, call its CID x1: parent = d1, name = guide, target = {kind: redirect, to: {space: alice, node: g1}}. A reader of /docs/guide/usage reaches x1, follows the redirect to g1, and continues down the remaining segment usage to u1.
Under HM24 this rename would have required new Refs for guide, install and usage, plus three redirect Refs, published in a sequence that could be interrupted, and Carol's path-scoped capability would have stopped matching.
URLs
Three URL forms name a resource.
Form | Example | Meaning |
|---|---|---|
Node URL |
| Canonical and mutable: the owner's account followed by the node id. A first path segment of 59 characters starting |
Pretty URL |
| Resolved top down through names and redirects. Breaks when a name changes and no redirect was left. |
Full-context link |
| What writers emit. Both the path and the id are present. |
Resolution of a full-context link tries the node id first: if the node named by n exists and the reader may read it, that is the target. Otherwise the pretty path is resolved. If both fail the link is broken. If both succeed and disagree, the id wins and the client may warn. This is the roadmap's decision that links are written with all available context and resolved through a fallback order.
The query parameters v (version) and the fragment #<block>[start:end] keep their meaning from URLs. Facets keep today's spelling after the resource: hm://<space>/bafyreib7x…:comments.
Legacy URLs hm://<space>/<path> written before Stem keep resolving: the migration gives each HM24 document the id of its earliest Ref blob and places it under the migrated parent chain with its last segment as its name, so the pretty path is unchanged. A name can never be mistaken for a node id, because names of that exact shape are refused.
Listings
A directory listing is the set of children of a node that the reader may read, with their names and the facts the indexer derives (title, kind, child count). A reader never learns of a child it may not read, including its name. Unnamed children appear in a listing to readers who may read them, shown by their id and title. See Privacy.
Today (HM24)
Today | Stem |
|---|---|
Hierarchy is the path prefix; a child's Ref repeats the parent's segments | Hierarchy is the |
Rename or move republishes every descendant plus redirects | One Node blob moves a subtree |
Redirect is a Ref shape at the old path | Redirect is a new node at the old name whose target points at the moved node |
Capabilities are scoped by path prefix and break on move | Grants are scoped by node id and follow the node |
Private documents must be single-segment paths with random names | Any node may be unnamed, at any depth |
Links carry a path, optionally a version | Links carry path and node id, resolved id first |
Open questions
Open: whether a client should offer to leave a redirect by default on every rename, or only when the old path has known inbound links.
Open: whether a Node blob should be allowed to name a parent in another space to express "mounted" content, or whether redirects are enough. Stem's position is that redirects are enough and identity stays within a space.
See also
Resources and nodes: identity and the fold.
Node and redirect target.
Authority: why a move needs write on both parents.
Migration: how legacy paths become ids.
Do you like what you are reading? Subscribe to receive updates.
Unsubscribe anytime