Quickback Docs

Feature Areas - Hierarchical Authz Grouping

Group features under an area folder with _area.ts. The area declares a route prefix, admission lanes, and an authz vocabulary that only its own subtree may reference.

Group related features under one area folder. The area's _area.ts file declares a route prefix, admission lanes, and an authorization vocabulary — relationships, roles, rules, tenant anchors, scope kinds — that every feature inside the folder inherits by name. Features outside the folder cannot reference any of it: the compiler fails the build.

Naming note: this concept is an area. domain in Quickback always means a DNS custom domain (see custom domains).

Why

Cross-cutting authz for a concept like a project, an event, or a location used to live in the root quickback.config.ts and was wired to its consumers by a path-prefix string across a flat feature list. An area makes the grouping structural: the folder is the boundary, the _area.ts is the single declaration site, and the compiler enforces that the vocabulary never leaks outside the subtree.

Layout

quickback/features/
├── workspace/                 ← AREA (has _area.ts)
│   ├── _area.ts               ← defineArea({...})
│   ├── projects.ts            ← the area folder may itself be a feature
│   ├── project_collaborators.ts
│   ├── tasks/                 ← child feature
│   │   ├── tasks.ts
│   │   └── actions/addTask.ts
│   └── chat/                  ← child feature
│       └── actions/postMessage.ts
└── billing/                   ← flat feature (unchanged)

Reserved sub-directories (actions/, handlers/, lib/, pages/) are never child features. A directory without _area.ts is a leaf feature — its unreserved subdirectories are not loaded, and a subdirectory containing defineTable(...) / defineAction(...) fails the compile (silent route/table disappearance after a mis-move is the failure mode areas must never introduce). Feature leaf names stay globally unique — they key src/features/<name>/ in the output, so moving a feature into an area changes nothing about its generated identity.

_area.ts

// features/workspace/_area.ts
import { defineArea } from '@quickback/compiler';

export default defineArea({
  // Namespace identity — drives the emitted routes file name and errors.
  name: 'projectScope',

  // Inherited route mount + admission gate. Optional: an area with no
  // prefix contributes shared vocabulary only. A prefix REQUIRES at least
  // one `via` lane (a mounted prefix with no gate would be ungated).
  prefix: '/project/:projectId',
  via: ['collaboratorOf', { roles: ['admin', 'owner'] }],
  mintScope: true,
  acceptScope: 'project',

  // Area-owned vocabulary — the EXACT same shapes as the root authz blocks.
  relationships: {
    collaboratorOf: {
      from: 'project_collaborators',
      subject: { column: 'collaboratorUserId', equals: 'ctx.userId' },
      resource: { column: 'projectId' },
      where: { status: 'active' },
      loads: 'projects',
      exposeAs: 'project',
    },
  },
  roles: {
    projectStaff: { or: [{ via: 'collaboratorOf' }, { roles: ['admin', 'owner'] }] },
  },
  scopes: {
    project: {
      requestField: 'projectId',
      roles: { collaborator: { via: 'collaboratorOf', subject: 'both' } },
    },
  },
  // Named gates + tenant anchors work too — same shapes as authz.rules /
  // authz.tenants, visible to this subtree only.
});

A descendant feature references everything by bare name — no imports, no root declaration:

// features/workspace/tasks/actions/addTask.ts
export default defineAction({
  path: './tasks/add',                    // relative → /project/:projectId/tasks/add
  access: { roles: ['projectStaff'] },    // resolves against the area
  // ...
});

The visibility contract

An area's names are visible to its own subtree only (the area folder, its features, and nested areas). The compiler checks every reference channel — access roles: arrays (including named gates and both spellings of scoped roles: bare aliases and scope:<kind>:<role>), action access and anchor:, firewall via: arms, ctx.scope.<kind> accessors, { rule } and { tenant } firewall references, auth-view tenant: includes, realtime.requiredRoles, table-level namespace.via lanes, and other _area.ts nodes. Referencing an area name from outside is always a compile error:

Feature "billing" ... references role "projectStaff" declared by area "projectScope"
(features/workspace/_area.ts), but this site is not inside that area. ... move the
referencing code under the declaring area, or lift the declaration to root authz if it
is genuinely global.

Two structural rules keep the fold sound:

  • No overrides (v1). Re-declaring an inherited name — in any class, including scope kinds, scoped-role aliases, and namespace names — is a compile error. Declare each name in exactly one place. (overrides: marker syntax is reserved for a future release.)
  • Declaration-site rule. Root authz bodies (roles, rules, tenants, scopes, namespaces) may reference root-declared names only — a root definition is visible everywhere and would launder an area name out of its subtree. An area's bodies may reference root ∪ ancestors ∪ self.

One deliberate exception: root-config namespace-identity wiring — today exactly realtime.wsTicket.namespace — may name an area's namespace. It attaches the area's complete gate (its full lane set) and constructs no new grant. Such consumers are listed in the manifest under the owning area.

Containment and the route: 'self' marker

The area's prefix is an audited surface:

  • A feature outside the area may not declare an action path (or a table-level namespace prefix) under the area's prefix — compile error.
  • A project-level authz.namespaces entry the area does not own — root-declared, or another area's — may not carve a prefix at or under the area's prefix. Namespace claiming is longest-prefix-wins, so such an entry would silently re-gate the area's descendant actions with its own via lanes — compile error. (A descendant feature's table-level namespace inside the prefix is legal; the manifest attributes its claimed actions to that namespace, not to the area.)
  • A descendant action whose absolute path is outside the prefix opts out of the area's admission gate and is gated solely by its own access:. That must be explicit: mark the action route: 'self', or the compile fails (a typo'd prefix must never silently self-gate). The marker is equally rejected where it is dead — on in-prefix actions and in features outside any mounted area. Every self-gated action is listed in the manifest.

Relative paths (path: './…') resolve against the area prefix. Dot segments (. / ..) are rejected outright — the resolved route must stay under the prefix.

Nested areas

Areas nest to arbitrary depth. A nested area may declare additional vocabulary freely. A nested area may not mount its own prefix under an ancestor that already mounts one: per-level nested admission is unlinked (a caller related to child instance S under parent A would be admitted at /a/A/s/S even when S belongs to parent B), and Quickback refuses to compile an unlinked gate. Mount one prefix per chain, or keep the nested area vocabulary-only.

The capability floor: requires: ['feature-areas']

An area-tree project must declare the marker in quickback.config.ts:

export default defineConfig({
  // ...
  requires: ['feature-areas'],
});

Any CLI — including one that predates areas — passthrough-executes the config, so the marker always reaches the compiler. A current compiler that sees the marker but no areas payload rejects the compile with an upgrade message instead of silently compiling a project whose nested features an old CLI never loaded (which would otherwise generate destructive migrations). In the other direction, a current CLI refuses output from a compiler that predates areas.

The authz manifest

Area projects emit quickback/authz-manifest.json on every compile — the audit inventory the flat config can no longer answer from one file: the area tree, every folded name and its declaring file, per-area member features, areaGatedActions vs selfGatedActions, and any root-config namespace consumers. Review it in PRs the way you review the security reports; the generated AGENTS.md also gains a feature-areas section so coding agents place new features correctly.

What belongs at root

Root authz remains the home for genuinely global vocabulary — names used across areas or by flat features. Everything scoped to one concept belongs in that concept's _area.ts. When in doubt: if only one folder's features reference it, it belongs in that folder's area.

On this page