Appearance
How It All Fits Together
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 - Schema files (
schema/rooms.ts) and feature configs (features/rooms/resource.ts,features/rooms/actions.ts) - Quickback compiles them - Analyzes your definitions at build time
- You get a production API -
GET /rooms,POST /rooms,PATCH /rooms/:id,DELETE /rooms/:id, plus custom actions likePOST /rooms/:id/activate
File Structure
Your definitions live in a definitions/ folder with two main areas:
definitions/
├── schema/ # Database tables (Drizzle ORM)
│ ├── users.ts
│ └── rooms.ts
│
└── features/ # Feature-by-feature configuration
└── {feature-name}/
├── resource.ts # Security config (firewall, access, guards, masking)
├── actions.ts # Custom actions/routes
└── handlers/ # Complex action handlers (optional)
└── my-action.tsSchema files define your database tables using Drizzle ORM. These are the raw table structures.
Resource files configure how each table is exposed via the API - who can access it, what fields can be modified, and how data is isolated.
Action files define custom business logic beyond basic CRUD operations.
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/team1. 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. If a table has organization_id, users in Org A can never see Org B's data, even if they guess the record ID.
You only need to define a firewall when:
- You need an
exceptionfor public/global data - Both
organization_idanduser_idexist and you need to clarify which takes priority
2. Access (CRUD Permissions)
Access controls which operations a user can perform. It checks roles and record conditions.
typescript
crud: {
list: { access: { roles: ["member", "admin"] } },
get: { access: { roles: ["member", "admin"] } },
create: { access: { roles: ["admin"] } },
update: { access: { roles: ["admin"] } },
delete: { access: { roles: ["admin"] } },
}You can also add conditions like "only update your own records" or "only delete drafts".
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 |
Example: A status field might be protected - users can't directly PATCH it, but they can call an approve action that changes it.
4. Masking (Data Redaction)
Masking hides sensitive fields from users who shouldn't see them.
typescript
masking: {
ssn: { mask: 'ssn' }, // Shows: ***-**-1234
email: { mask: 'email' }, // Shows: j***@example.com
salary: { mask: 'redact' }, // Shows: [REDACTED]
}You can conditionally show the real value based on roles or ownership.
How They Work Together
Here's a real example of how a request flows through all four layers:
Scenario: A member requests GET /employees/123
Firewall checks: Is employee 123 in the user's organization?
- ✅ Yes → Continue
- ❌ No → 404 Not Found (as if it doesn't exist)
Access checks: Can members perform GET?
- ✅ Yes → Continue
- ❌ No → 403 Forbidden
Guards don't apply to GET (they're for writes)
Masking applies: User is a member, not admin
- SSN:
123-45-6789→***-**-6789 - Salary:
85000→[REDACTED] - Email:
john@company.com→j***@company.com
- SSN:
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 guard for each action |
This means:
- A resource with no definition = completely inaccessible
- Forgot to add a field to
createable? = 400 error on create - No
accessrule? = 403 forbidden
You must deliberately open each door. This prevents accidental data exposure.
Next Steps
Now that you understand the mental model, dive into the specifics: