Definitions Overview
Understand how Quickback's security layers work together. Learn the mental model for firewall, access, guards, and masking to build secure APIs.
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.
- You write definitions - Table files with schema and security config using
defineTable - Quickback compiles them - Analyzes your definitions at build time
- 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.jsonTable files use defineTable to combine schema and security config:
// 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 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/teamSecurity Applies Everywhere
These security layers protect both API responses and Realtime broadcasts. When you enable Realtime, 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.
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:.
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"] } },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 |
4. Masking (Data Redaction)
Masking hides sensitive fields from users who shouldn't see them.
masking: {
email: { type: 'email' }, // Shows: j***@e******.com
phone: { type: 'phone' }, // Shows: ******4567
salary: { type: 'redact' }, // Shows: [REDACTED]
}How They Work Together
Scenario: An interviewer requests GET /candidates/cnd_123
-
Firewall checks: Is candidate
cnd_123in the user's organization?- Yes → Continue
- No → 404 Not Found (as if it doesn't exist)
-
Access checks: Can interviewers perform GET?
- Yes → Continue
- No → 403 Forbidden
-
Guards don't apply to GET (they're for writes)
-
Masking applies: User is an
interviewer, notrecruiterorhiring-manager- Email:
jane@company.com→j***@c******.com - Phone:
555-123-4567→******4567
- Email:
-
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 |
references | Explicit FK target table for columns that don't follow the "Id"-stripped convention (e.g. vendorId → contact). | 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 |
defaultSort | Default sort order for CMS list rendering ({ field, order: "asc" | "desc" }). | CMS Table Views |
inputHints | Per-column CMS input type: select, richtext, textarea, lookup, currency, etc. Pure CMS hint, no runtime effect. | 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 |
embeddings | Auto-generate vector embeddings for specified fields on insert/update. | Stack → Embeddings |
realtime | Per-table broadcast config (enabled, onInsert, onUpdate, onDelete, requiredRoles, fields). | Stack → 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
- Database Schema — Define your tables
- Firewall — Set up data isolation
- Access — Configure read and write permissions
- Guards — Control field modifications
- Masking — Hide sensitive data
- Views — Column-level security
- Validation — Field validation rules
- Actions — Add custom business logic