# Permissions

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`:

```typescript
// 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](/define/scopes) |
| `{ pseudoRole: 'AUTHENTICATED' }` | `PUBLIC` / `AUTHENTICATED` / `INTERNAL` / … |
| `{ arrowRef: 'eventOrg', permission: 'org:admin' }` | an [FK-traversal arrow](/define/arrows) — 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:

```typescript
// 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](/define/firewall#named-permissions-permission)
for how the arm sits inside a firewall.

## Firewall-pushable permission rules

A firewall permission compiles all the way down to a SQL `WHERE` expression.
The production-ready subset includes relationship leaves, composed
`{ permissionRef }` leaves, and FK-traversal arrows under `anyOf` / `allOf`.
Those rules run in the database alongside the rest of the row firewall.

Two expression shapes belong at a different enforcement site, and the compiler
routes you there with a build error instead of weakening the policy:

- **Claim checks.** `{ role }`, `{ scopeRole }`, and `{ pseudoRole }` are
  decided from verified request claims rather than row subqueries. Put them on
  `read.access` / `crud.*.access`, then keep the permission focused on row
  reachability.
- **Negated SQL relationships.** `not` over a relationship site is refused
  because nullable `NOT IN (subquery)` semantics create the classic null trap.
  Express the positive relationship set or enforce the exclusion at a runtime
  access site.

The wider DSL remains available for permission definitions and future
enforcement sites, while every firewall reference is checked for SQL
pushability today. FK-traversal arrows are firewall-pushable; see
[Arrows](/define/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](/define/diagnostics).

## Reference

```typescript
// 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 `from` table's own firewall (tenant scope + soft-delete) is AND'd into
  each subquery, exactly as with a `via:` arm.
- See [Scopes & capability grants](/define/scopes) for scoped
  roles and [Access — relationships](/define/relationships#authzrelationships)
  for the relationship shape.
