Getting Started
Get started with Quickback in minutes. Learn how to define database tables with security configuration and compile them into a production-ready API.
Get started with Quickback in minutes. This guide shows you how to define a complete table with security configuration.
Start a Project
mkdir my-app && cd my-app
npx @quickback-dev/cli startquickback start is interactive — it asks for a template and project name, scaffolds the project on disk, and only prompts for an account just before compiling. If you cancel at the login prompt the scaffold stays put; finish later with quickback login && quickback compile.
Already know what you want? Pass the template (and optionally a name) to skip the prompts:
npx @quickback-dev/cli start blank # config only, no features
npx @quickback-dev/cli start todos # working CRUD example
npx @quickback-dev/cli start blog my-site # blog starter, named my-sitePrefer a scriptable form?
npm install -g @quickback-dev/cli
quickback create cloudflare my-app
cd my-appBoth paths scaffold the same project:
quickback.config.ts— Project configurationquickback/features/— Your table definitions- Working example feature with full security configuration (when you pick
todos/blog/saas)
Available templates:
cloudflare— Cloudflare Workers + D1 + Better Auth, bare scaffoldtodos— Working CRUD example with masking + actionsblog— Single-tenant blog, PUBLIC reads, admin writesblank— Cloudflare scaffold, no example features (alias:empty)saas— Full B2B SaaS with orgs, R2 file storage, webhooks
File Structure
Each table gets its own file with schema and security config together:
quickback/
├── quickback.config.ts
└── features/
└── jobs/
├── jobs.ts # Table + security config
├── applications.ts # Related table + config
├── actions.ts # Custom actions (optional)
└── handlers/ # Action handlers (optional)
└── close-job.tsComplete Example
Here's a complete jobs table with all security layers in a single feature() call:
// quickback/features/jobs/jobs.ts
import { feature, q } from '@quickback/compiler';
export default feature('jobs', {
columns: {
id: q.id(),
title: q.text().required(),
department: q.text().required(),
status: q.text().default('draft').required(), // draft | open | closed
salaryMin: q.int().optional(),
salaryMax: q.int().optional(),
// Ownership — required for firewall data isolation
organizationId: q.scope("organization"),
},
// 1. FIREWALL — Data isolation
firewall: [{ field: 'organizationId', equals: 'ctx.activeOrgId' }],
// 2. GUARDS — Field modification rules
guards: {
createable: ["title", "department", "status", "salaryMin", "salaryMax"],
updatable: ["title", "department", "status"],
},
// 3. READ — Collection + per-id GET (gates GET / and GET /:id)
read: {
access: { roles: ["owner", "hiring-manager", "recruiter", "interviewer"] },
pageSize: 25,
},
// 4. WRITE OPERATIONS
create: { access: { roles: ["owner", "hiring-manager"] } },
update: { access: { roles: ["owner", "hiring-manager"] } },
delete: { access: { roles: ["owner", "hiring-manager"] }, mode: "soft" },
});One import, one export, every security layer declared in one place. Secure by default — no write routes ship until you opt in with top-level create, update, delete, or upsert.
Alternatives — same result, different shape
If you'd rather split the table declaration from the security config (e.g. to reference the table in an action file), use the two-export form. Identical output:
import { q, defineTable } from '@quickback/compiler';
export const jobs = q.table('jobs', {
id: q.id(),
title: q.text().required(),
// … same columns
});
export default defineTable(jobs, {
firewall: [{ field: 'organizationId', equals: 'ctx.activeOrgId' }],
// … same config
});
export type Job = typeof jobs.$infer;Or if you're interop'ing with existing Drizzle schemas, you can author with sqliteTable / pgTable directly — the compiler dispatches per file, and all forms can coexist in the same project.
See feature() — the canonical form and Schema: Drizzle interop for the full picture.
What Each Layer Does
- Firewall: Automatically adds
WHERE organizationId = ?to every query. Users in Org A can never see Org B's data. - Guards: Controls which fields can be modified —
createablefor POST,updatablefor PATCH,protectedfor action-only fields. - Write Access: Role-based access control for each operation. All roles can read, only hiring managers can write.
Compile and Run
# Compile your definitions — prompts you to sign in or sign up
# if you haven't already (no separate `quickback login` step needed)
quickback compile
# Run locally
npm run devThe first compile prompts for an account; subsequent compiles re-use the stored session.
Local development requirements
Two things the local wrangler dev server needs that production gets from Cloudflare config:
BETTER_AUTH_SECRET— read from a.dev.varsfile in the project root. Projects scaffolded withquickback start/createget one generated automatically (git-ignored). If yours is missing, every auth call returns503 MISSING_ENV; create it with:echo "BETTER_AUTH_SECRET=$(openssl rand -hex 32)" > .dev.vars- An
Originheader on cookie-backed writes — CSRF protection rejects mutations (403 CSRF_ORIGIN_REJECTED) unless the request carries a trusted origin. Browsers send it automatically; when testing with curl or an agent, add-H "Origin: http://localhost:8787"to POST/PATCH/DELETE requests (reads don't need it). Bearer-token requests are exempt.
Generated Endpoints
Quickback generates these endpoints from the example above:
| Method | Endpoint | Description |
|---|---|---|
GET | /api/v1/jobs | List jobs (all roles) |
GET | /api/v1/jobs/:id | Get single job |
POST | /api/v1/jobs | Create job (hiring managers only) |
PATCH | /api/v1/jobs/:id | Update job (hiring managers only) |
DELETE | /api/v1/jobs/:id | Soft delete job (hiring managers only) |
Test Your API
After quickback compile and npm run dev, your API is running locally. Open a second terminal and try these requests:
# 1. Create a user account
curl -X POST http://localhost:8787/auth/v1/sign-up/email \
-H "Content-Type: application/json" \
-d '{"email": "admin@example.com", "password": "securepassword123", "name": "Admin"}'
# 2. Sign in and get a session token
curl -X POST http://localhost:8787/auth/v1/sign-in/email \
-H "Content-Type: application/json" \
-d '{"email": "admin@example.com", "password": "securepassword123"}'
# → Response includes a session token in Set-Cookie header
# 3. Create a record (use the session cookie from step 2)
curl -X POST http://localhost:8787/api/v1/jobs \
-H "Content-Type: application/json" \
-H "Cookie: better-auth.session_token=<token>" \
-d '{"title": "Senior Engineer", "department": "Engineering", "status": "open"}'
# 4. List records
curl http://localhost:8787/api/v1/jobs \
-H "Cookie: better-auth.session_token=<token>"Cloudflare templates run on port 8787 (wrangler dev). Check your terminal output for the exact URL.
Next Steps
- Template Walkthroughs — Detailed setup guides
- Full Example — Complete resource walkthrough
- Database Schema — Column types, relations, audit fields
- Firewall — Data isolation patterns
- Access — Role & condition-based control
- Guards — Field modification rules
- Masking — Field redaction for sensitive data
- Actions — Custom business logic endpoints
See Also
- Quickback Stack — The runtime environment where your compiled API runs (D1, KV, R2, auth)
- Account UI — Pre-built authentication and account management UI
- Using the API — CRUD endpoints, filtering, and batch operations