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