Documents in Seed Hypermedia are versioned through Change blobs. Each change is a signed, content-addressed blob that builds upon previous versions. This guide explains how changes work and how to create them programmatically.
The Change Model
Instead of storing document state directly, SHM stores a chain of changes (like Git commits). Each change contains operations that modify the document.
Empty Mermaid block
Each change blob contains:
{
"@type": "Change",
"deps": ["bafy2bz..."], // CIDs of parent changes
"author": "z6Mk...", // Account ID of author
"action": "Update", // "Update" or "Create"
"timestamp": 1700000000, // Unix seconds
"ops": [...], // List of operations
"sig": "base58-signature" // Ed25519 signature
}Change Operations
Four operations modify documents:
Empty Mermaid block
1. SetMetadata
Sets document-level metadata (title, description, icon, etc).
{
"set_metadata": {
"key": "name",
"value": "My Document Title"
}
}Common metadata keys: name, description, icon, thumbnail
2. ReplaceBlock
Creates a new block or updates an existing one. The block ID determines whether it's a create or update.
{
"replace_block": {
"id": "block-unique-id",
"type": "Paragraph",
"text": "Hello world!",
"attributes": {},
"annotations": []
}
}IMPORTANT: ReplaceBlock only sets block content. You MUST also use MoveBlock to position it in the document tree, otherwise the block exists but isn't visible!
3. MoveBlock
Positions a block in the document tree. This is how you create hierarchy.
{
"move_block": {
"block_id": "block-to-move",
"parent": "parent-block-id", // "" for root level
"left_sibling": "sibling-id" // "" for first position
}
}Empty Mermaid block
4. DeleteBlock
Removes a block from the document.
{
"delete_block": "block-id-to-delete"
}Note: Deleting a parent doesn't automatically delete children. Children become orphaned and won't render.
Creating a Document: Full Example
To create a document with a heading and paragraph, you need both ReplaceBlock and MoveBlock operations:
grpcurl -plaintext -d '{
"account": "z6Mk...",
"path": "/my-doc",
"baseVersion": "",
"signingKeyName": "my-key",
"changes": [
{"set_metadata": {"key": "name", "value": "My Document"}},
{"replace_block": {
"id": "h1",
"type": "Heading",
"text": "Welcome",
"attributes": {"level": "2"}
}},
{"move_block": {"block_id": "h1", "parent": "", "left_sibling": ""}},
{"replace_block": {
"id": "p1",
"type": "Paragraph",
"text": "This is content under the heading."
}},
{"move_block": {"block_id": "p1", "parent": "h1", "left_sibling": ""}}
]
}' localhost:55002 com.seed.documents.v3alpha.Documents/CreateDocumentChangeThis creates:
Empty Mermaid block
Updating Documents
To update, provide the current version as baseVersion. The daemon will create a new change that depends on it.
// 1. Get current version
const doc = await getDocument(account, path);
const baseVersion = doc.version; // "bafy2bz..."
// 2. Submit changes with baseVersion
await createDocumentChange({
account,
path,
baseVersion, // Links to previous version
changes: [
{replace_block: {id: "p1", type: "Paragraph", text: "Updated text!"}}
]
});If someone else edited the document, you'll get a conflict error. Fetch the latest version and retry.
Version Resolution
SHM uses a DAG (Directed Acyclic Graph) for versions. Multiple authors can create changes in parallel, and the system merges them.
Empty Mermaid block
Conflicts on the same block are resolved by timestamp (last-write-wins), but different blocks can be edited independently.
Common Patterns
Append a block to the end
// Find the last root-level block
const lastBlock = doc.content.slice(-1)[0];
changes.push(
{replace_block: {id: "new", type: "Paragraph", text: "Appended!"}},
{move_block: {
block_id: "new",
parent: "",
left_sibling: lastBlock?.block?.id || ""
}}
);Replace all content
// 1. Delete all existing blocks
for (const blockId of getAllBlockIds(doc)) {
changes.push({delete_block: blockId});
}
// 2. Add new content
for (const node of newContent) {
changes.push({replace_block: node.block});
changes.push({move_block: {block_id: node.block.id, ...}});
}Insert under a heading
// Add paragraph as child of heading "h1"
changes.push(
{replace_block: {id: "p-new", type: "Paragraph", text: "Under heading"}},
{move_block: {
block_id: "p-new",
parent: "h1", // Parent is the heading
left_sibling: "" // First child
}}
);Blob Signing
Change blobs are cryptographically signed. The daemon handles this automatically via CreateDocumentChange, but if you're building custom tooling:
Empty Mermaid block
See /signing for detailed blob signing documentation.