# Definitions Overview

Before diving into specific features, let's understand how Quickback's pieces connect. This page gives you the mental model for everything that follows.

## The Big Picture

Quickback is a **backend compiler**. You write definition files, and Quickback compiles them into a production-ready API.

1. **You write definitions** - Table files with schema and security config using `feature()`
2. **Quickback compiles them** - Analyzes your definitions at build time
3. **You get a production API** - `GET /jobs`, `POST /jobs`, `PATCH /jobs/:id`, `DELETE /jobs/:id`, batch operations, plus custom actions

## Tenancy is not yours to model

**Workspace, team, tenant, account, org — they are all the Better Auth organization.** Quickback ships multi-tenancy; you never build it.

- **Don't** create a `workspaces` / `teams` / `tenants` table.
- **Don't** create a membership or `*_members` table.
- **Do** put `organizationId: q.scope("organization")` on every tenant-scoped table — the firewall is auto-derived from it.
- Membership and roles arrive on every request as `ctx.activeOrgId` and `ctx.roles`. Do not query AUTH_DB for the **acting** user. To check that a *submitted* user id belongs to the org, query AUTH_DB from an action — see [Scoped DB](/define/actions/scoped-db#reaching-better-auth-tables-authdb).
- Create a tenant with `POST /auth/v1/organization/create` and switch with `POST /auth/v1/organization/set-active` — not a custom action.

If your product calls it something else, that's a **label, not a schema decision** — the Account UI renames the organization surfaces for you (see [Account → Customization](/ui/account/customization)).

> **The expensive mistake**
>
> Hand-rolling a tenant table means re-implementing membership, roles, and invitations — and none of it is wired to `ctx`. Every access rule you then write against it fights the compiler, because `authz.relationships` cannot read Better Auth's tables.


## The authorization ladder

Four rungs, in order. **Most apps never leave rung 1.** Climb only when the rung you're on genuinely can't express the rule — each one costs concepts, compile-time constraints, and per-request work.

| Rung | Reach for it when | Skip it when |
|---|---|---|
| **1. Org roles + firewall** | `organizationId: q.scope('organization')` plus `roles: ['member+']` / `['admin+']`. No `authz` block at all. | — this is the floor. It covers the large majority of apps end to end. |
| **2. Named rules** — [`authz.roles` / `authz.rules`](/define/authz-roles#centrally-defined-access-rules) | the same role expression repeats across many features and you want one source of truth | you have only a couple of repeats — inline them |
| **3. Relationships + areas** — [`authz.relationships`](/define/relationships), [feature areas](/define/areas) | membership lives in **your own** domain table (an attendee, a collaborator, an external reviewer), or a subtree shares a URL prefix **and** a per-row admission gate | org membership already answers it — that's rung 1 |
| **4. FGA** — [OpenFGA / Zanzibar](/define/fga) | you need a permission *graph*: derived, inherited, hierarchical, or public share links | a single hop answers it |

**Rung 3 is never the answer to "is this person in the org?"** A relationship's `from:` must be one of *your* feature tables — the compiler rejects one pointing at Better Auth's `organization_memberships`, `organizations`, or `user`. Org membership is rung 1, and it is already decided before your handler runs.

## File Structure

Your definitions live in a `quickback/features/` folder organized by feature:

```
my-app/
├── quickback/
│   ├── quickback.config.ts    # Compiler configuration
│   └── features/
│       └── {feature-name}/
│           ├── candidates.ts       # Table + config (feature())
│           ├── applications.ts     # Secondary table + config
│           ├── interview-scores.ts # Internal table (no routes)
│           ├── actions/            # Custom actions — one file per action
│           │   └── advanceStage.ts
│           ├── lib/                # Feature-local helpers (optional)
│           └── pages/              # CMS page definitions (optional)
├── src/                       # Generated code (output)
└── package.json
```

`actions/`, `lib/`, and `pages/` are the **reserved** sub-directories of a
feature — nothing else inside a feature folder is loaded, and a stray subdirectory that
contains `defineTable(...)` / `defineAction(...)` files fails the compile (files silently
disappearing after a mis-move is the failure mode this guards against).

Features can also be grouped into **areas**: a folder carrying an `_area.ts`
(`export default defineArea({...})`) declares a route prefix + admission gate and a shared
authz vocabulary, and every unreserved child directory becomes a child feature (nesting to
arbitrary depth):

```
quickback/features/
├── project/                   # AREA (has _area.ts)
│   ├── _area.ts               # defineArea({...}) — prefix, via lanes, vocabulary
│   ├── projects.ts            # the area folder may itself be a feature
│   └── tasks/                 # child feature
│       ├── tasks.ts
│       └── actions/addTask.ts
└── billing/                   # flat feature (unchanged)
```

An area gates a **row inside a tenant** (this project, this event, this location) — it is not how you get multi-tenancy, which you already have. Area projects must declare `requires: ['feature-areas']` in `quickback.config.ts`. See [Feature areas](/define/areas) for the full model.

**Table files** use `feature()` to combine schema and security config in one call:

```typescript
// features/candidates/candidates.ts
import { feature, q } from "@quickback/compiler";

export default feature("candidates", {
  columns: {
    id:             q.id(),
    organizationId: q.scope("organization"),
    name:           q.text().required(),
    email:          q.text().required(),
    phone:          q.text().optional(),
    source:         q.text().optional(),
    ...q.audit(),
    ...q.softDelete(),
  },
  firewall: [{ field: 'organizationId', equals: 'ctx.activeOrgId' }],
  guards: {
    createable: ["name", "email", "phone", "source"],
    updatable: ["name", "phone"],
  },
  masking: {
    email: { type: 'email', show: { roles: ['owner', 'hiring-manager', 'recruiter'] } },
    phone: { type: 'phone', show: { roles: ['owner', 'hiring-manager', 'recruiter'] } },
  },
  read: {
    access: { roles: ["owner", "hiring-manager", "recruiter", "interviewer"] },
  },
  create: { access: { roles: ["owner", "hiring-manager", "recruiter"] } },
  update: { access: { roles: ["owner", "hiring-manager", "recruiter"] } },
  delete: { access: { roles: ["owner", "hiring-manager"] } },
});
```

Consumers get the row type from the default export — `type Candidate = typeof import("./candidates").default.$infer`.

**Key points:**
- Tables with `export default feature(...)` (or `defineTable(...)`) → resource routes generated
- Tables without a default export → internal/junction tables (no routes)
- Route path derived from filename: `applications.ts` → `/api/v1/applications`

> **1 Resource = 1 Security Boundary**
>
> Each `feature()` / `defineTable()` defines its own complete security boundary — its own firewall (who sees the data), access rules (role requirements), guards (field validation), and masking (redaction). These are compiled into a single resource file that wraps all generated routes for that table.
>
> A table **without** a default export is a supporting table — used internally by actions, joins, or background jobs, but never exposed as its own API endpoint. See [Internal Tables](/define/schema#internal-tables-no-routes) for details.


## The Five Security Layers

Every API request passes through five security layers, in order:

```
Request → Rate Limit → Firewall → Access → Guards → Masking → Response
              │           │         │        │         │
              │           │         │        │         └── Hide sensitive fields
              │           │         │        └── Block field modifications
              │           │         └── Check roles & conditions
              │           └── Isolate data by owner/org/team
              └── Cap requests per user / op
```

> **Security Applies Everywhere**
>
> These security layers protect both **API responses** and **Realtime broadcasts**. When you enable [Realtime](/platform/realtime/durable-objects), the same masking rules apply to WebSocket messages - ensuring users only see data they're authorized to view.


### 1. Firewall (Data Isolation)

The firewall controls **which records** a user can see. It automatically adds WHERE clauses to every query based on your schema columns:

| Column in Schema | What happens |
|------------------|--------------|
| `organizationId` (or `organisationId` / `orgId` / `organization` / `organisation` / `org`) | Data isolated by organization — `WHERE organizationId = ctx.activeOrgId` |
| `ownerId` | Data isolated by user — `WHERE ownerId = ctx.userId`, and the column is auto-stamped on insert |
| `teamId` | Data isolated by team — `WHERE teamId = ctx.activeTeamId` |
| `userId` | **Not** auto-detected. On a feature table it usually means *a user this row references* (member, contact, invitee), not *the row's owner*. |

Detection matches either spelling of a column — the JS key (`organizationId`) or its SQL name (`organization_id`).

Exactly one isolation column must match: zero is an error, two or more is an "ambiguous firewall" error that asks you to declare `firewall:` explicitly. `userId` is the one column that looks like isolation but isn't — see [Firewall → `ownerId` vs `userId`](/define/firewall#ownerid-vs-userid).

For relationship-based row visibility (e.g. "user can see this event because they have a confirmed `event_guests` row pointing at it"), declare the relationship under `authz.relationships` and reference it from the firewall with a `via:` predicate. See [Firewall → Relationship-based scoping](/define/firewall#relationship-based-scoping-via).

[Learn more about Firewall →](/define/firewall)

### 2. Access (CRUD Permissions)

Access controls **which operations** a user can perform. It checks roles and record conditions. Reads (`GET /` and `GET /:id`) live under `read:`; writes live under top-level `create:`, `update:`, `delete:`, and `upsert:`.

```typescript
read: {
  access: { roles: ["owner", "hiring-manager", "recruiter", "interviewer"] },
},
create: { access: { roles: ["owner", "hiring-manager", "recruiter"] } },
update: { access: { roles: ["owner", "hiring-manager", "recruiter"] } },
delete: { access: { roles: ["owner", "hiring-manager"] } },
```

[Learn more about Access →](/define/access)

### 3. Guards (Field Modification Rules)

Guards control **which fields** can be modified in each operation.

| Guard Type | What it means |
|------------|---------------|
| `createable` | Fields that can be set when creating |
| `updatable` | Fields that can be changed when updating |
| `protected` | Fields that can only be changed via specific actions |
| `immutable` | Fields that can never be changed after creation |

[Learn more about Guards →](/define/guards)

### 4. Masking (Data Redaction)

Masking hides sensitive fields from users who shouldn't see them.

```typescript
masking: {
  email: { type: 'email' },       // Shows: j***@e******.com
  phone: { type: 'phone' },       // Shows: ******4567
  salary: { type: 'redact' },     // Shows: [REDACTED]
}
```

[Learn more about Masking →](/define/masking)

### 5. Rate Limiting (Request Caps)

Rate limiting caps **how often** an operation can be called, backed by Cloudflare's native Rate Limiting binding — no extra storage, no third-party libraries. Unlike the other four layers, it's applied to **every resource by default** (read `1000/60s`, writes `200/60s`) and runs first, before any data is touched.

```typescript
rateLimit: {
  delete: { limit: 5, period: 60 },  // tighter than the 200/60s default
},
// read / create / update keep shipped defaults
```

Override per-op, per-resource, or project-wide; set `false` at any scope to opt out. Limits are keyed by `<resource>:<op>:<userId>` (falling back to client IP for anonymous callers), and over-cap requests get a `429` with `Retry-After`.

[Learn more about Rate Limiting →](/define/rate-limit)

## How They Work Together

**Scenario:** An interviewer requests `GET /candidates/cnd_123`

1. **Rate Limit** checks: Is this user under their `read` cap on `candidates`?
   - Yes → Continue
   - No → 429 Too Many Requests (with `Retry-After`)

2. **Firewall** checks: Is candidate `cnd_123` in the user's organization?
   - Yes → Continue
   - No → 404 Not Found (as if it doesn't exist)

3. **Access** checks: Can interviewers perform GET?
   - Yes → Continue
   - No → 403 Forbidden

4. **Guards** don't apply to GET (they're for writes)

5. **Masking** applies: User is an `interviewer`, not `recruiter` or `hiring-manager`
   - Email: `jane@company.com` → `j***@c******.com`
   - Phone: `555-123-4567` → `******4567`

6. **Response** sent with masked data

## Locked Down by Default

Quickback is **secure by default**. Nothing is accessible until you explicitly allow it.

| Layer | Default | What you must do |
|-------|---------|------------------|
| Firewall | AUTO | Auto-detects from an `organizationId` / `ownerId` / `teamId` column (`userId` is **not** a candidate). Declare `firewall:` explicitly when none matches, when two do, or to opt out with `[{ exception: true }]`. |
| Access | DENIED | Explicitly define `access` rules with roles |
| Guards | LOCKED | Explicitly list `createable`, `updatable` fields |
| Rate Limit | ON | Applied to every resource (read 1000/60s, write 200/60s). Override or set `false` to opt out. |
| Actions | BLOCKED | Explicitly define `access` for each action |

**You must deliberately open each door.** This prevents accidental data exposure.

## Other Resource Options

Beyond the five security layers, `defineTable` accepts a handful of declarative hints that shape the generated API and the CMS without touching your security posture.

| Option | What it does | Full reference |
|---|---|---|
| `views` | Named column projections — `GET /resource/views/{name}` returns a subset of columns under its own access rule. | [Views](/define/views) |
| `transitions` | Guarded state changes — each entry generates a real `POST /resource/:id/{name}` action file, with guards, stamps, idempotency, `undo` inverses, and `onEnter` cascades. | [Transitions](/define/transitions) |
| `changesets` | An `owns` boundary — write the parent row and its owned relations in one atomic request, each op paying the child's own firewall, access, and guards. | [Changesets](/define/changesets) |
| `triggers` | `before`/`after` hooks on `insert`/`update`/`delete`, lowering either to a real SQL trigger in your migrations or to an application handler at the write chokepoint. | [Triggers](/define/triggers) |
| `references` | Explicit FK target table for columns that don't follow the "Id"-stripped convention (e.g. `vendorId → contact`). | [CMS Schema Registry](/ui/admin/schema-registry) |
| `displayColumn` | Column to use as the human-readable label in FK resolutions and CMS list views (auto-detected as `name`/`title`/`label` if not set). | [CMS Table Views](/ui/admin/table-views) |
| `defaultSort` | Default sort order for CMS list rendering (`{ field, order: "asc" \| "desc" }`). | [CMS Table Views](/ui/admin/table-views) |
| `inputHints` | Per-column CMS input type: `select`, `richtext`, `textarea`, `lookup`, `currency`, etc. Pure CMS hint, no runtime effect. | [CMS Record Layouts](/ui/admin/record-layouts) |
| `layouts` | Named record-page layouts — group fields into sections (collapsed, multi-column). Falls back to auto-grouping if unset. | [CMS Record Layouts](/ui/admin/record-layouts) |
| `embeddings` | Auto-generate vector embeddings for specified fields on insert/update. | [Stack → Embeddings](/platform/vector/using-embeddings) |
| `realtime` | Per-table broadcast config (`enabled`, `onInsert`, `onUpdate`, `onDelete`, `requiredRoles`, `fields`). | [Stack → Realtime](/platform/realtime/using-realtime) |
| `path` | Override the route base path (defaults to feature name). | — |

All options are optional — omit them and Quickback uses sensible defaults (auto-detected display column, auto-grouped layout, no embeddings, no realtime).

## Next Steps

1. [Database Schema](/define/schema) — Define your tables
2. [Firewall](/define/firewall) — Set up data isolation
3. [Access](/define/access) — Configure read and write permissions
4. [Guards](/define/guards) — Control field modifications
5. [Masking](/define/masking) — Hide sensitive data
6. [Rate Limit](/define/rate-limit) — Per-operation request caps
7. [Views](/define/views) — Column-level security
8. [Validation](/define/validation) — Field validation rules
9. [Transitions](/define/transitions) — Guarded state changes, generated as real endpoints
10. [Changesets](/define/changesets) — Atomic parent-plus-owned-relations writes
11. [Triggers](/define/triggers) — Data rules and side-effects on write
12. [Actions](/define/actions) — Custom business logic, for what the above can't express
