Skip to content

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

PermissionDescription
'any'Any authenticated user
'none'No one (explicitly deny)
'uid'Document owner (the uid field)
BufferA 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:

ReferenceResolves 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 rules
  • context — 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 allowed
  • null — 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.