Skip to content

Documents

A document is a plain object with some system-managed fields. You define the shape; the store handles hashing, versioning, and sync.

Adding documents

Every document has a type and an owner (uid):

typescript
const [errors, hash] = await store.add('bookmark', {
    uid,                    // owner identity (Buffer)
    url: 'https://example.com',
    title: 'Example',
});

if (errors) {
    console.log('Validation failed:', errors);
} else {
    console.log('Created:', hash.toString('hex'));
}

The returned hash is the document's identity — a SHA-256 of its CBOR-encoded content.

Type registration

Register types before using them:

typescript
store.registerType('bookmark');

Rendered documents include the system fields (createdAt, updatedAt, uid, parent, hash) plus your content fields.

Render options

By default, rendered documents get createdAt (genesis timestamp) and updatedAt (timestamp of the latest applied edit, or createdAt if none). You can rename these fields or disable them per type:

typescript
// Custom field names
store.registerType('activity', {
    render: { createdAt: 'created', updatedAt: 'modified' }
});

// Disable rendered timestamps
store.registerType('raw-data', {
    render: { createdAt: false, updatedAt: false }
});

Type-bound collections

For write operations, get a type-bound collection — the type is set automatically:

typescript
const bookmarks = store.type('bookmark');

// No need to pass type — it's bound
const [errors, hash] = await bookmarks.add({
    uid,
    url: 'https://example.com',
    title: 'Example',
});

System fields

These are managed by the store — don't set them yourself:

FieldTypeDescription
hashBufferContent hash (computed on add)
createdAtnumberCreation timestamp on rendered/internal docs (passable to add() to set explicitly; otherwise Date.now()). Renameable per type.
updatedAtnumberLast-edit timestamp on rendered/internal docs (passable to edit() as { updatedAt }; otherwise Date.now()). Renameable per type.
rootBufferOriginal document hash (for edits)
prevBufferPrevious version hash (for edits)
luidBufferLocal user identity — see luid

time is the wire-level change-record envelope timestamp (CBOR-hashed and signed for sync). It does not appear on rendered docs — apps see createdAt/updatedAt.

User-settable fields

These are yours to set:

FieldTypeDescription
uidBufferDocument owner (required)
parentBufferParent document hash (for hierarchies)
shareobjectSharing policy (who can sync this)
writeobjectWrite rules (who can edit what)

Plus any app-specific fields you want.

luid

luid (local user identity) tags every document with which local identity received or created it. When your app handles multiple identities on the same device, luid keeps their data separate in one database.

luid is passed as a separate parameter, not as part of the document content:

typescript
// Creating a document — luid identifies which local identity this belongs to
const [errors, hash] = await store.add('note', {
    uid,
    title: 'Hello',
    share: { self: true },
}, luid);

// Receiving a document from sync — luid comes from the peer context
await store.inject(envelope, luid);

What makes luid special

  • Not part of the content hash — the same document received by different local identities has the same hash
  • Immutable — once set at creation, it can't be changed
  • Stored in persistence — available on rendered documents for filtering queries
  • Not synced — each device sets its own luid when storing documents; it's local-only metadata

How sync uses it

The sync protocol sets luid automatically from the peer context. When Alice's sync session pulls documents, they're tagged with Alice's identity. When Bob's session pulls, they're tagged with Bob's. The getRoots callback receives luid as the uid parameter:

typescript
const sync = new SyncProtocol({
    store,
    getRoots: async (uid) => {
        // uid here is the luid — return roots for this specific identity
        return await getRootsForIdentity(uid);
    },
});

Sync endpoints filter by luid to prevent cross-identity data leaks — a peer connected through Alice's identity only sees documents tagged with Alice's luid.

Parent-child relationships

Documents can form trees via the parent field:

typescript
// Create a folder
const [, folderHash] = await store.add('folder', {
    uid,
    name: 'Work bookmarks',
});

// Create a bookmark under it
const [, bookmarkHash] = await store.add('bookmark', {
    uid,
    parent: folderHash,
    url: 'https://example.com',
    title: 'Example',
});

Parent documents can define write rules for their children — see Write Rules.

Deletion

Soft delete preserves the document for sync (peers learn about the deletion) and supports restore:

typescript
// Delete (cascades to children)
await store.delete(uid, hash);

// Restore
await store.restore(uid, hash);

Under the hood, delete creates an edit with $delete: true that syncs like any other edit.

Signatures

When configured with sign and verify callbacks, every document and edit is cryptographically signed by its author. This is how peers verify authenticity during sync.

typescript
const store = new DocumentStore({
    storage: new SurrealdbPersistence('surrealkv://./data'),
    sign: async (uid, hash, claim) => {
        // Sign with wish-core identity
        return await app.identitySign(uid, hash, claim);
    },
    verify: async (uid, hash, signature, claim) => {
        return await app.identityVerify(uid, hash, signature, claim);
    },
});
await store.storage.ready();

Without signatures, documents are still hashed and versioned, but peers can't verify authorship.