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.
ownerField (the Convention)
Section titled “ownerField (the Convention)”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)A Custom Resolver
Section titled “A Custom Resolver”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;When Ownership Can’t Be Verified
Section titled “When Ownership Can’t Be Verified”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 userac.grant('admin').createAny('folderShare'); // any folder → any user
const folder = await db.getFolder(folderId); // { ownerId: ... }ac.can(role, { user, folderShare: folder }) .createOwn('folderShare').granted;