Appearance
Write Rules
Write rules control who can edit which fields. Set them on a document's write field when creating it.
Basic structure
typescript
await store.add('post', {
uid,
title: 'Hello',
write: {
'*': 'uid', // Default: only owner can edit fields
title: 'any', // Anyone can edit the title
write: 'uid', // Only owner can change write rules
$delete: 'uid', // Only owner can delete
}
});'*' is the default rule — applies to any field not explicitly listed.
Permission types
| Permission | Description |
|---|---|
'any' | Any authenticated user |
'none' | No one (explicitly deny) |
'uid' | Document owner (the uid field) |
Buffer | A specific identity |
{ role: string } | User with this role in members array |
[perm, ...] | OR — any matching permission grants access |
Examples
typescript
write: {
// Only the owner
'*': 'uid',
// Owner OR a specific admin
'*': ['uid', adminUid],
// Anyone with admin role in members array
'*': { role: 'admin' },
// Owner OR admin role
'*': ['uid', { role: 'admin' }],
// No one can edit (immutable field)
createdBy: 'none',
}Child document rules
Parent documents can define rules for child document creation and editing:
typescript
await store.add('folder', {
uid,
name: 'Shared Folder',
write: {
'*': 'uid',
$child: {
bookmark: {
$create: 'any', // Anyone can create bookmarks here
'*': 'uid', // Bookmark author can edit their own
$delete: ['uid', '^uid'], // Author OR folder owner can delete
}
}
}
});$create
Controls who can create child documents of this type under this parent.
$delete
Controls who can delete. Often combined with '^uid' (parent owner).
Parent reference (^)
The ^ prefix resolves a field from the parent document:
| Reference | Resolves to |
|---|---|
'^uid' | Parent document's uid (owner) |
'^moderators' | Parent document's moderators field |
'^fieldname' | Any field on the parent |
typescript
// Discussion owned by video creator
write: {
$child: {
comment: {
$create: 'any',
'*': 'uid',
$delete: ['uid', '^uid'], // comment author OR video owner
}
}
}Role-based access
Documents with a members array can use role-based permissions:
typescript
await store.add('workspace', {
uid,
name: 'Team',
members: [
{ userId: aliceUid, role: 'admin' },
{ userId: bobUid, role: 'editor' },
{ userId: carolUid, role: 'viewer' },
],
write: {
'*': ['uid', { role: 'admin' }],
content: ['uid', { role: 'admin' }, { role: 'editor' }],
members: 'uid', // Only owner manages membership
}
});The permission resolver checks if the editing user has a matching role in the members array.
Field-level rules
Beyond simple permissions, fields support extended rules for fine-grained control:
Immutable fields
Prevent a field from being changed after document creation:
typescript
write: {
'*': 'uid',
slug: { allow: 'uid', immutable: true },
}The slug field can be set when creating the document, but any subsequent edit to it will be rejected.
Conditional denial (unless)
Deny edits when the document is in a specific state:
typescript
write: {
title: { allow: 'any', unless: { published: true } },
}Anyone can edit the title — unless the document has published: true. The condition is checked against the document's current state.
Array add/remove permissions
For array fields, you can set separate permissions for adding and removing items:
typescript
write: {
members: {
allow: { role: 'admin' }, // General field access
add: { allow: { role: 'admin' } }, // Who can add items
remove: { allow: { role: 'admin' } }, // Who can remove items
},
}This is useful for member lists where you might want different rules for joining vs being removed. When using $push or $addToSet, the add permission is checked. When using $pull or $pullAll, the remove permission is checked. If add/remove aren't specified, the field's allow permission is used for all operations.
Custom permission resolver
For app-specific permission types that go beyond the built-in system, pass a resolvePermission callback to the store:
typescript
const store = new DocumentStore({
storage: new SqlitePersistence(db),
resolvePermission: async (permission, context) => {
// Handle custom permission types
if (permission.require === 'workspace-admin') {
const isAdmin = await checkWorkspaceAdmin(context.uid);
return { allowed: isAdmin ? [context.uid] : 'none' };
}
// Return null to fall through to built-in resolution
return null;
},
});The resolver receives:
permission— the permission value from write rulescontext— the validation context (uid, document, parent, etc.)
Return values:
{ allowed: Buffer[] }— list of allowed user IDs{ allowed: 'any' }— anyone is allowed{ allowed: 'none' }— no one is allowednull— fall through to built-in resolution
Ownership transfer
The uid field is writable — ownership can be transferred:
typescript
await store.edit(currentOwner, { hash }, {
$set: { uid: newOwner }
});The version history preserves the original creator.