Appearance
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:
| Field | Type | Description |
|---|---|---|
hash | Buffer | Content hash (computed on add) |
createdAt | number | Creation timestamp on rendered/internal docs (passable to add() to set explicitly; otherwise Date.now()). Renameable per type. |
updatedAt | number | Last-edit timestamp on rendered/internal docs (passable to edit() as { updatedAt }; otherwise Date.now()). Renameable per type. |
root | Buffer | Original document hash (for edits) |
prev | Buffer | Previous version hash (for edits) |
luid | Buffer | Local 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:
| Field | Type | Description |
|---|---|---|
uid | Buffer | Document owner (required) |
parent | Buffer | Parent document hash (for hierarchies) |
share | object | Sharing policy (who can sync this) |
write | object | Write 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
luidwhen 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.