Hosted onhoraciohdev.dev.hyper.mediavia theHypermedia Protocol

Developer Guide: How Publishing a Document Works in Seed

Publishing a document in Seed means creating and storing signed blobs that describe the document state. A document is not “saved” as one mutable database row. Instead, publishing creates content-addressed blobs that the daemon indexes and syncs.

Core Concepts

1. Change blob

Change blob contains document operations:

    set metadata/title

    add or replace blocks

    move blocks

    delete blocks

The Change is signed by the author/signer.

For a new document, the first Change can also be the document genesis.

2. Ref blob

Ref blob points a document path to a version.

Example:

hm://account/path -> Ref -> heads: [Change CID] -> genesisBlob: Change CID

Without a valid indexed Ref, the document may exist as raw blobs, but it will not appear as a normal document resource.

3. Capability blob

Capability blob proves that a signer can write under another account.

This is required when publishing to an account different from the signer’s own account.

Example:

Seed Surveys account grants WRITER to Nodos signer

For cross-account publishing, the publish payload should include:

Capability blob + Change blob + Ref blob

SDK Publishing Flow

The TypeScript SDK flow is the preferred model when the client/server code signs the document itself.

This is the same general model used by the CLI.

Step 1: Build document operations

Document content is converted into operations.

Example operation types:

SetAttributes ReplaceBlock MoveBlocks DeleteBlocks

For markdown, the CLI/SDK parses markdown into Seed blocks and then flattens them into document operations.

Step 2: Create unsigned change

const {unsignedBytes, ts} = createChangeOps({ ops, })

This creates unsigned CBOR bytes for the Change.

Step 3: Sign the change

const changeBlock = await createChange(unsignedBytes, signer)

This produces:

{ bytes, cid, }

The CID becomes the document version.

Step 4: Create signed Ref

const refInput = await createVersionRef( { space: accountUid, path: '/document-path', genesis: changeCid, version: changeCid, generation: Number(ts), capability: capabilityCid, visibility: 'Private', // only for private docs }, signer, )

The Ref makes the document path point to the new version.

Step 5: Publish blobs

await client.publish({ blobs: [ capabilityBlob, // when publishing cross-account {cid: changeCid, data: changeBlock.bytes}, ...refInput.blobs, ], })

This calls:

POST /api/PublishBlobs

which maps to daemon:

daemon.storeBlobs(...)

The daemon stores and indexes the blobs.

Cross-Account Publishing

If signer and destination account are different:

signerAccountUid !== destinationAccountUid

then the signed Ref must include a valid capability CID.

Also, the capability blob itself must be present in the daemon/index. Otherwise the Ref may not index as a valid writable resource.

So publish:

Capability + Change + Ref

not only:

Change + Ref

Public vs Private Documents

Public documents omit visibility in the Ref.

Private documents include:

visibility: 'Private'

Private documents must use a simple one-segment path, for example:

/private-feedback-abc

not:

/folder/private-feedback-abc

Daemon CreateDocumentChange Alternative

There is also a daemon-native path:

grpcClient.documents.createDocumentChange(...)

This asks the daemon to create/sign/index the Change and Ref.

However, this path checks write permissions before publishing. That means the daemon must already know the signer has write access.

For cross-account publishing, this can fail if the capability is not already indexed locally:

permission_denied: key is not allowed to write to space

That is why SDK signing can be better for browser/server workflows where we explicitly publish the capability blob together with the document blobs.

Syncing After Publish

After local publish, the document may need to be pushed to another peer:

grpcClient.resources.pushResourcesToPeer({ resources: [documentId], addrs: peerAddrs, recursive: false, })

This is a sync optimization, not the core publish step.

A document can be successfully created locally even if push announces zero blobs or fails. The important thing is that the daemon stored and indexed:

Capability + Change + Ref

Mental Model

Publishing a document means:

Build operations -> Create Change -> Sign Change -> Create Ref -> Sign Ref -> Publish blobs -> Daemon indexes blobs -> Optional peer push

For cross-account publishing:

Fetch/include Capability -> Publish Capability + Change + Ref together

The Ref is the key object that makes a document visible at an hm://account/path resource.

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

Unsubscribe anytime