Skip to content

Ownership

In v2, readOwn / updateOwn only chose which attributes applied — confirming the record actually belonged to the user was your job. v3 can enforce ownership for you.

Tell AccessControl which field holds the owner id, then pass both the user and the record in the check context. Ownership is context.user.id === context.<resource>[ownerField].

const ac = new AccessControl({}, { policy: { ownerField: 'ownerId' } });
ac.grant('user').updateOwn('order', ['*']);
ac.can('user', { user: { id: 7 }, order: { ownerId: 7 } })
.updateOwn('order').granted; // true (owned)
ac.can('user', { user: { id: 7 }, order: { ownerId: 9 } })
.updateOwn('order').granted; // false (not owned)

For anything beyond a single field (composite keys, membership, async‑free lookups against the context), provide policy.owner(ctx). It wins over ownerField.

const ac = new AccessControl({}, {
policy: {
owner: (ctx) =>
ctx.doc?.authorId === ctx.user?.id ||
ctx.doc?.editors?.includes(ctx.user?.id)
}
});
ac.grant('writer').updateOwn('doc', ['*', '!audit']);
ac.can('writer', { user, doc }).updateOwn('doc').granted;

With a resolver configured but the record (or owner) missing from the context, the check is denied under the default strict.checks: true — fail closed.

Record-level Rules without Hand-rolled Checks

Section titled “Record-level Rules without Hand-rolled Checks”

Express “can assign own folder to any user” as ownership + possession, then let the engine decide:

const ac = new AccessControl({}, { policy: { ownerField: 'ownerId' } });
ac.grant('user').createOwn('folderShare'); // own folder → any user
ac.grant('admin').createAny('folderShare'); // any folder → any user
const folder = await db.getFolder(folderId); // { ownerId: ... }
ac.can(role, { user, folderShare: folder })
.createOwn('folderShare').granted;