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/CreateDocumentChange

      This 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.