$ variable references
Compare two fields on the same request — $subject.id, $resource.attributes.ownerId, $env.ip — for owner checks, attribute matching, and self-action prevention.
What are $-references?
Dollar-prefixed values resolve at evaluation time instead of being literals. Use them to compare two fields on the same request.
// This checks: resource.attributes.ownerId === request.subject.id
.when((w) => w.check('resource.attributes.ownerId', 'eq', '$subject.id'))// This checks: resource.attributes.ownerId === request.subject.id
.when((w) => w.check('resource.attributes.ownerId', 'eq', '$subject.id'))$subject.id isn't compared literally. At eval time the engine strips $, resolves subject.id from the request, then compares.
isOwner shortcut
The most common pattern — owner check — has a built-in helper:
// These are equivalent:
.when((w) => w.isOwner())
.when((w) => w.check('resource.attributes.ownerId', 'eq', '$subject.id'))// These are equivalent:
.when((w) => w.isOwner())
.when((w) => w.check('resource.attributes.ownerId', 'eq', '$subject.id'))Custom owner field name:
.when((w) => w.isOwner('resource.attributes.createdBy'))
// resource.attributes.createdBy === $subject.id.when((w) => w.isOwner('resource.attributes.createdBy'))
// resource.attributes.createdBy === $subject.idWhere $-references work
$-references work anywhere a value is accepted — .check(), .attr(), .resourceAttr(), .env(), and the shorthand operator methods (.eq, .neq, etc.):
// Compare resource owner to current user
.when((w) => w.resourceAttr('ownerId', 'neq', '$subject.id'))
// Compare subject attribute to resource attribute
.when((w) => w.attr('status', 'eq', '$resource.attributes.status'))
// Prevent self-actions (e.g. user can't delete own account)
.when((w) => w.check('resource.id', 'eq', '$subject.id'))
// Compare env value to resource attribute
.when((w) => w.env('ip', 'eq', '$resource.attributes.allowedIp'))
// Resource department must match subject department
.when((w) => w.check(
'resource.attributes.department',
'eq',
'$subject.attributes.department',
))
// Resource scope must match request scope
.when((w) => w.check('resource.attributes.scope', 'eq', '$scope'))// Compare resource owner to current user
.when((w) => w.resourceAttr('ownerId', 'neq', '$subject.id'))
// Compare subject attribute to resource attribute
.when((w) => w.attr('status', 'eq', '$resource.attributes.status'))
// Prevent self-actions (e.g. user can't delete own account)
.when((w) => w.check('resource.id', 'eq', '$subject.id'))
// Compare env value to resource attribute
.when((w) => w.env('ip', 'eq', '$resource.attributes.allowedIp'))
// Resource department must match subject department
.when((w) => w.check(
'resource.attributes.department',
'eq',
'$subject.attributes.department',
))
// Resource scope must match request scope
.when((w) => w.check('resource.attributes.scope', 'eq', '$scope'))What can $-paths point at?
The same roots that field paths can use:
$subject.*—$subject.id,$subject.attributes.foo,$subject.roles$resource.*—$resource.id,$resource.type,$resource.attributes.foo$environment.*—$environment.ip,$environment.timestamp,$environment.foo$action— shorthand for the action string$scope— shorthand for the scope string
Type-safe autocomplete
When you use createAccessConfig() with a typed context, $-references get full autocomplete in your editor. Type '$' in any value position and your editor suggests all valid paths.
const access = createAccessConfig({
actions: ['read', 'update'] as const,
resources: ['post'] as const,
context: {} as {
subject: { id: string; attributes: { tier: 'free' | 'pro' } }
resource: { type: 'post'; attributes: { ownerId: string; tier: 'free' | 'pro' } }
},
})
access.policy('post-tier').rule('match-tier', (r) =>
r
.allow()
.on('read')
.of('post')
// Editor autocompletes "$subject.attributes.tier" here
.when((w) => w.check('resource.attributes.tier', 'eq', '$subject.attributes.tier')),
)const access = createAccessConfig({
actions: ['read', 'update'] as const,
resources: ['post'] as const,
context: {} as {
subject: { id: string; attributes: { tier: 'free' | 'pro' } }
resource: { type: 'post'; attributes: { ownerId: string; tier: 'free' | 'pro' } }
},
})
access.policy('post-tier').rule('match-tier', (r) =>
r
.allow()
.on('read')
.of('post')
// Editor autocompletes "$subject.attributes.tier" here
.when((w) => w.check('resource.attributes.tier', 'eq', '$subject.attributes.tier')),
)Internally, the & {} intersection trick is used so that even when the literal type is string, autocomplete still shows $-prefixed suggestions. See the type-safe config docs.
Common patterns
Owner-only edits
.rule('owner-edit', (r) =>
r
.allow()
.on('update', 'delete')
.of('post')
.when((w) => w.isOwner()), // resource.attributes.ownerId === $subject.id
).rule('owner-edit', (r) =>
r
.allow()
.on('update', 'delete')
.of('post')
.when((w) => w.isOwner()), // resource.attributes.ownerId === $subject.id
)Same-tenant access
.rule('tenant-isolation', (r) =>
r
.allow()
.on('*')
.of('document')
.when((w) =>
w.check('resource.attributes.tenantId', 'eq', '$subject.attributes.tenantId'),
),
).rule('tenant-isolation', (r) =>
r
.allow()
.on('*')
.of('document')
.when((w) =>
w.check('resource.attributes.tenantId', 'eq', '$subject.attributes.tenantId'),
),
)Prevent self-deletion
.rule('no-self-delete', (r) =>
r
.deny()
.on('delete')
.of('user')
.when((w) => w.check('resource.id', 'eq', '$subject.id')),
).rule('no-self-delete', (r) =>
r
.deny()
.on('delete')
.of('user')
.when((w) => w.check('resource.id', 'eq', '$subject.id')),
)Deny when attributes diverge
.rule('classification-match-required', (r) =>
r
.deny()
.on('read')
.of('document')
.when((w) =>
w.check('subject.attributes.clearance', 'lt', '$resource.attributes.classificationLevel'),
),
).rule('classification-match-required', (r) =>
r
.deny()
.on('read')
.of('document')
.when((w) =>
w.check('subject.attributes.clearance', 'lt', '$resource.attributes.classificationLevel'),
),
)