# 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 `defineTable`
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

## 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 (defineTable)
│           ├── applications.ts     # Secondary table + config
│           ├── interview-scores.ts # Internal table (no routes)
│           ├── actions.ts          # Custom actions (optional)
│           └── handlers/           # Action handlers (optional)
│               └── advance-stage.ts
├── src/                       # Generated code (output)
└── package.json
```

**Table files** use `defineTable` to combine schema and security config:

```typescript
// features/candidates/candidates.ts
import { sqliteTable, text } from "drizzle-orm/sqlite-core";
import { defineTable } from "@quickback/compiler";

export const candidates = sqliteTable("candidates", {
  id: text("id").primaryKey(),
  organizationId: text("organization_id").notNull(),
  name: text("name").notNull(),
  email: text("email").notNull(),
  phone: text("phone"),
  source: text("source"),
});

export default defineTable(candidates, {
  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"] } },
});

export type Candidate = typeof candidates.$inferSelect;
```

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

> **1 Resource = 1 Security Boundary**
>
> Each `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** `defineTable()` is a supporting table — used internally by actions or handlers, but never exposed as its own API endpoint. See [Internal Tables](/compiler/definitions/schema#internal-tables-no-routes) for details.


## The Four Security Layers

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

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

> **Security Applies Everywhere**
>
> These security layers protect both **API responses** and **Realtime broadcasts**. When you enable [Realtime](/stack/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 |
|------------------|--------------|
| `organization_id` | Data isolated by organization |
| `user_id` | Data isolated by user (personal data) |

No manual configuration needed - Quickback applies smart rules based on your schema.

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](/compiler/definitions/firewall#relationship-based-scoping-via).

[Learn more about Firewall →](/compiler/definitions/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 →](/compiler/definitions/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 →](/compiler/definitions/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 →](/compiler/definitions/masking)

## How They Work Together

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

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

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

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

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

5. **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 `organization_id`/`user_id` columns. Only configure for exceptions. |
| Access | DENIED | Explicitly define `access` rules with roles |
| Guards | LOCKED | Explicitly list `createable`, `updatable` fields |
| Actions | BLOCKED | Explicitly define `access` for each action |

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

## Other Resource Options

Beyond the four 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](/compiler/definitions/views) |
| `references` | Explicit FK target table for columns that don't follow the "Id"-stripped convention (e.g. `vendorId → contact`). | [CMS Schema Registry](/stack/cms/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](/stack/cms/table-views) |
| `defaultSort` | Default sort order for CMS list rendering (`{ field, order: "asc" \| "desc" }`). | [CMS Table Views](/stack/cms/table-views) |
| `inputHints` | Per-column CMS input type: `select`, `richtext`, `textarea`, `lookup`, `currency`, etc. Pure CMS hint, no runtime effect. | [CMS Record Layouts](/stack/cms/record-layouts) |
| `layouts` | Named record-page layouts — group fields into sections (collapsed, multi-column). Falls back to auto-grouping if unset. | [CMS Record Layouts](/stack/cms/record-layouts) |
| `embeddings` | Auto-generate vector embeddings for specified fields on insert/update. | [Stack → Embeddings](/stack/vector/using-embeddings) |
| `realtime` | Per-table broadcast config (`enabled`, `onInsert`, `onUpdate`, `onDelete`, `requiredRoles`, `fields`). | [Stack → Realtime](/stack/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](/compiler/definitions/schema) — Define your tables
2. [Firewall](/compiler/definitions/firewall) — Set up data isolation
3. [Access](/compiler/definitions/access) — Configure read and write permissions
4. [Guards](/compiler/definitions/guards) — Control field modifications
5. [Masking](/compiler/definitions/masking) — Hide sensitive data
6. [Views](/compiler/definitions/views) — Column-level security
7. [Validation](/compiler/definitions/validation) — Field validation rules
8. [Actions](/compiler/definitions/actions) — Add custom business logic
