Permissions
Name a grant rule once with anyOf / allOf / not over relationships, org roles, scoped roles, and pseudo-roles — then reference it from any firewall as { permission: 'x' }.
A via: arm references one relationship. A roles: [...] array ORs a
flat list of role names. When the same grant rule recurs across several
resources — "organizer or confirmed attendee", "admin and not
suspended" — name it once under authz.permissions and reference it
everywhere by name. A permission is a named boolean expression over the
authorization primitives you've already declared.
Declaring permissions
Permissions live in quickback.config.ts alongside authz.relationships:
// quickback.config.ts
import { defineConfig } from '@quickback/compiler';
export default defineConfig({
// ...providers...
authz: {
relationships: {
organizerOf: { from: 'event_staff', subject: { column: 'userId', equals: 'ctx.userId' }, resource: { column: 'eventId' }, where: { role: 'organizer' } },
attendeeOf: { from: 'event_guests', subject: { column: 'userId', equals: 'ctx.userId' }, resource: { column: 'eventId' }, where: { status: 'confirmed' } },
},
permissions: {
'event:view': { anyOf: ['organizerOf', 'attendeeOf'] },
'event:manage': { allOf: ['organizerOf', { not: 'suspendedFromEvent' }] },
'org:admin': { anyOf: [{ role: 'admin' }, { role: 'owner' }] },
},
},
});The expression DSL
A permission is a tree of three combinators over leaves:
| Combinator | Meaning |
|---|---|
{ anyOf: [...] } | union — satisfied if any arm holds |
{ allOf: [...] } | intersection — satisfied only if all arms hold |
{ not: <expr> } | exclusion — satisfied if the operand does not hold |
A bare leaf (no combinator) is a one-arm permission — the replacement for a
single via:.
Leaves
A leaf names one authorization primitive. The common case is a bare string; object forms are the unambiguous escape hatch.
| Leaf | Resolves to |
|---|---|
'organizerOf' (bare string, no prefix) | a declared relationship |
{ relationRef: 'organizerOf' } | a declared relationship (explicit) |
{ permissionRef: 'event:view' } | another permission (composition) |
{ role: 'admin' } | an org-membership role (ctx.roles) |
{ scopeRole: { kind: 'event', role: 'organizer' } } | a scoped role |
{ pseudoRole: 'AUTHENTICATED' } | PUBLIC / AUTHENTICATED / INTERNAL / … |
{ arrowRef: 'eventOrg', permission: 'org:admin' } | an FK-traversal arrow — resolve the row's FK to its target object and require a permission there |
String sugar for the prefixed forms is also accepted inside an expression:
'permission:event:view', 'scope:event:organizer', 'role:admin'. A bare
string with no recognized prefix is always a relationship reference.
Referencing a permission
A resource references a permission from its firewall with a
{ permission } arm. field is the resource-side column the lowered
subqueries constrain — the same column a via: arm would carry:
// features/sessions/sessions.ts
firewall: [
{ field: 'organizationId', equals: 'ctx.activeOrgId' },
{ field: 'eventId', permission: 'event:view' },
]The compiler lowers the permission to the equivalent firewall arms:
anyOf becomes an OR of relationship subqueries (any), allOf becomes an
AND (all), and { permissionRef } inlines the referenced permission's
arms. The result is byte-identical to writing those via: arms by hand — the
permission is just the named, reusable spelling.
See Firewall — named permissions for how the arm sits inside a firewall.
M1 limits
Permissions are richer than what a firewall can enforce today. The DSL
accepts the full shape so configs are forward-compatible, but a firewall
runs as a SQL WHERE clause, so a permission referenced from a firewall
must be SQL-pushable. Two cases are rejected at compile time when
referenced from a firewall:
- Claim-site leaves.
{ role },{ scopeRole }, and{ pseudoRole }are decided from the request's verified claims, not from a row subquery. A permission containing one of these can't lower to aWHEREclause, so referencing it from a firewall is a compile error. Gate membership onread.access/crud.*.accessinstead, where role checks belong. not. Anotwhose operand resolves to a SQL (relationship) site is rejected —NOT IN (subquery)over a nullable set is the classic null-trap (aNULLrow silently passes). It's representable in the DSL for future claim/runtime sites but errors in a firewall in M1.
These shapes ({ role }, not, typed caveats) remain in the DSL because
they're the enforcement surfaces of later milestones (claim-site and runtime
checks). M1 ships the SQL-pushable subset: relationship and { permissionRef }
leaves under anyOf / allOf. FK-traversal arrows ({ arrowRef, permission })
are now shipped and are firewall-pushable — see
Arrows.
The compile-time analyzer flags permissions that can never be satisfied,
arms shadowed by a broader arm, a not over a SQL site (the null-trap), and
the subquery / CTE cost of each rule — see
Diagnostics.
Reference
// authz.permissions.<name>
type PermissionExpr =
| PermissionLeaf
| { anyOf: PermissionExpr[] }
| { allOf: PermissionExpr[] }
| { not: PermissionExpr };
type PermissionLeaf =
| string // relationship name (bare) or prefixed sugar
| { relationRef: string }
| { permissionRef: string }
| { role: string } // claim-site — not firewall-pushable (M1)
| { scopeRole: { kind: string; role: string } } // claim-site — not firewall-pushable (M1)
| { pseudoRole: string } // claim-site — not firewall-pushable (M1)
| { arrowRef: string; permission: string }; // FK-traversal arrow — see Arrows- Referenced from a firewall as
{ field, permission: '<name>' }. - Relationship leaves carry every property of the underlying relationship —
the
fromtable's own firewall (tenant scope + soft-delete) is AND'd into each subquery, exactly as with avia:arm. - See Scopes & capability grants for scoped roles and Access — relationships for the relationship shape.
Scopes & Capability Grants
Let non-org principals (event attendees, shuttle drivers, vendors) reach a narrow slice of your data through a server-verified, relationship-derived scope carried in a signed JWT — with least-privilege scoped roles enforced across every pillar.
Arrows
Traverse a foreign key to inherit authority from a parent object — "you can see this row because you administer the organization it belongs to", or "because you control an ancestor in the tree". Declared once under authz.arrows, referenced from a permission, lowered to an FK-hop subquery or a bounded WITH RECURSIVE CTE.