@kardoe/quickback 0.5.10 → 0.5.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -47,7 +47,7 @@ export const DOCS = {
47
47
  },
48
48
  "account-ui": {
49
49
  "title": "Account UI",
50
- "content": "# Quickback Account UI\n\nA production-ready React application providing complete authentication and account management functionality. Works with any Better Auth backend or Quickback-compiled API.\n\n## Overview\n\nAccount UI is a standalone SPA that provides:\n\n- **Authentication flows** - Login, signup, password reset, email verification\n- **Account management** - Profile editing, password changes, session management\n- **Organizations** - Multi-tenant organization management with roles and invitations\n- **Passkeys** - WebAuthn/FIDO2 passwordless authentication\n- **Admin panel** - User and subscription management\n- **API key management** - Generate and manage API keys for programmatic access\n\n## Features\n\n### Authentication\n\n- Email/password authentication with secure password requirements\n- Passkey (WebAuthn) support for passwordless login\n- Magic link authentication via email\n- Email OTP verification\n- Email verification flow\n- Password reset with secure tokens\n- Session management across devices\n\n### Account Management\n\n- User profile editing with avatar upload\n- Password management\n- Device and session management\n- Two-factor authentication setup\n- Account deletion with confirmation\n\n### Organizations\n\n- Multi-tenant organization support\n- Role-based access control (owner, admin, member)\n- Member invitation system\n- Organization settings management\n- Organization creation and deletion\n\n### Admin Features\n\n- User management dashboard\n- Subscription management\n- User creation and deletion\n- Password reset for users\n- Session management\n- Ban/unban users\n\n## Quick Start\n\nAccount UI can be consumed in two ways:\n\n### Option A: Standalone Template\n\nClone the repo and own the source — edit anything you want:\n\n```bash\nnpx degit Kardoe-com/quickback-better-auth-account-ui my-account-app\ncd my-account-app\nnpm install\n```\n\nConfigure environment variables, build, and deploy:\n\n```bash\ncp .env.example .env.development\nnpm run build\nnpx wrangler deploy\n```\n\n### Option B: Library Import\n\nInstall as a dependency and get updates via `npm update`:\n\n```bash\nnpm install quickback-better-auth-account-ui\n```\n\n```tsx\n\nsetAppConfig({\n authRoute: 'quickback',\n name: 'My App',\n companyName: 'My Company',\n});\n```\n\nSee the [Library Usage](/account-ui/library-usage) guide for full details.\n\n### Which Mode Should I Use?\n\n| | Standalone | Library |\n|---|---|---|\n| **Install** | `npx degit` | `npm install` |\n| **Customize** | Edit source directly | `setAppConfig()` + CSS |\n| **Updates** | Manual (re-clone/merge) | `npm update` |\n| **Best for** | Full control, heavy customization | Embedding in existing apps, staying up to date |\n\n## Architecture\n\n### Standalone Mode\n\nThe Account UI runs as a **standalone SPA** that:\n\n1. **Runs separately** from your main application\n2. **Communicates** with your API backend via REST\n3. **Serves** from its own subdomain (e.g., `account.example.com`)\n4. **Redirects** users to your main app after authentication\n\n**Why standalone?** Security isolation, reusability across apps, edge performance via Cloudflare, and independent maintenance.\n\n### Library Mode\n\nWhen imported as a library, the Account UI becomes a **component in your existing React app**:\n\n1. **Renders inside** your application's router\n2. **Shares** your app's React and router context\n3. **Configured** via `setAppConfig()` — no env files needed\n4. **Updated** via `npm update` — no manual merging\n\n**Why library?** Seamless integration, automatic updates, and simpler deployment when you already have a React app.\n\n## Configuration\n\n### Standalone (Template)\n\nSince you own the source code, you can customize the Account UI by:\n\n1. **Environment variables** — Set `VITE_*` variables for build-time configuration\n2. **Edit source directly** — Modify `src/config/app.ts` for defaults, labels, messages, routes, and features\n3. **Runtime overrides** — Call `setAppConfig()` in `src/main.tsx` for programmatic configuration\n\n### Library\n\nWhen using as a library, configure entirely via `setAppConfig()`:\n\n```ts\n\nsetAppConfig({\n authRoute: 'quickback',\n name: 'My App',\n companyName: 'My Company',\n tagline: 'Build faster',\n});\n```\n\nSee the following guides for detailed configuration:\n\n- **[Environment Variables](/account-ui/environment-variables)** - Complete list of all configuration options\n- **[Feature Flags](/account-ui/features)** - Enable/disable features\n- **[Customization](/account-ui/customization)** - Branding, labels, and messaging\n- **[Worker Setup](/account-ui/worker)** - Cloudflare Worker configuration\n\n## Integration with Your App\n\n### Redirecting to Account UI\n\nFrom your main application, redirect users to the Account UI for authentication:\n\n```ts\n// Redirect to login\nwindow.location.href = 'https://account.example.com/login';\n\n// Redirect to profile\nwindow.location.href = 'https://account.example.com/profile';\n\n// Redirect to organization\nwindow.location.href = 'https://account.example.com/organizations/acme-corp';\n```\n\n### Returning to Your App\n\nAfter authentication, the Account UI can redirect users back to your application:\n\n```bash\n# Set the main app URL\nVITE_APP_URL=https://app.example.com\n\n# Optional: Set tenant URL pattern\nVITE_TENANT_URL_PATTERN=/organizations/{slug}\n```\n\nWhen a user logs in, they'll be redirected to `VITE_APP_URL`. When they access an organization, they'll be sent to the tenant URL (e.g., `https://app.example.com/organizations/acme-corp`).\n\n## API Integration\n\nThe Account UI connects to your API backend. The API path structure depends on your auth route mode:\n\n### Better Auth Mode (default)\n\n```\nhttps://api.example.com/api/auth/sign-in/email\nhttps://api.example.com/api/auth/sign-up/email\nhttps://api.example.com/api/auth/get-session\nhttps://api.example.com/api/auth/passkey/register\n```\n\n### Quickback Mode\n\n```\nhttps://api.example.com/auth/v1/sign-in/email\nhttps://api.example.com/auth/v1/sign-up/email\nhttps://api.example.com/auth/v1/get-session\nhttps://api.example.com/auth/v1/passkey/register\n```\n\nSee [Standalone Usage](/account-ui/standalone) for Better Auth backends or [With Quickback](/account-ui/with-quickback) for Quickback-compiled backends.\n\n## Project Structure\n\n```\nmy-account-app/\n├── src/\n│ ├── main.tsx # App entry point\n│ ├── App.tsx # Router & routes\n│ ├── worker.ts # Cloudflare Worker entry\n│ ├── auth/\n│ │ └── authClient.ts # Better Auth client\n│ ├── config/\n│ │ ├── app.ts # App config, labels, messages, features\n│ │ ├── features.ts # Feature flag helpers\n│ │ ├── routes.ts # Route helpers\n│ │ └── runtime.ts # Runtime API URL resolution\n│ ├── pages/ # Page components\n│ ├── components/ # Shared UI components\n│ ├── layouts/ # Auth & public layouts\n│ └── lib/ # API client, utilities\n├── .env.example # Environment template\n├── wrangler.toml # Cloudflare Worker config\n├── vite.config.ts # Vite build config\n└── tailwind.config.ts # Tailwind CSS config\n```\n\n## Next Steps\n\n- **[Standalone Usage](/account-ui/standalone)** - Use with any Better Auth backend (template mode)\n- **[With Quickback](/account-ui/with-quickback)** - Use with a Quickback-compiled backend\n- **[Library Usage](/account-ui/library-usage)** - Import as an npm package into your existing React app\n- **[Environment Variables](/account-ui/environment-variables)** - Configure your deployment\n- **[Feature Flags](/account-ui/features)** - Enable optional features\n- **[Customization](/account-ui/customization)** - Match your brand\n- **[Worker Setup](/account-ui/worker)** - Deploy to Cloudflare"
50
+ "content": "# Quickback Account UI\n\nA production-ready React application providing complete authentication and account management functionality. Works with any Better Auth backend or Quickback-compiled API.\n\n## Overview\n\nAccount UI is a standalone SPA that provides:\n\n- **Authentication flows** - Login, signup, password reset, email verification\n- **Account management** - Profile editing, password changes, session management\n- **Organizations** - Multi-tenant organization management with roles and invitations\n- **Passkeys** - WebAuthn/FIDO2 passwordless authentication\n- **Admin panel** - User and subscription management\n- **API key management** - Generate and manage API keys for programmatic access\n\n## Features\n\n### Authentication\n\n- Email/password authentication with secure password requirements\n- Passkey (WebAuthn) support for passwordless login\n- Magic link authentication via email\n- Email OTP verification\n- Email verification flow\n- Password reset with secure tokens\n- Session management across devices\n\n### Account Management\n\n- User profile editing with avatar upload\n- Password management\n- Device and session management\n- Two-factor authentication setup\n- Account deletion with confirmation\n\n### Organizations\n\n- Multi-tenant organization support\n- Role-based access control (owner, admin, member)\n- Member invitation system\n- Organization settings management\n- Organization creation and deletion\n\n### Admin Features\n\n- User management dashboard\n- Subscription management\n- User creation and deletion\n- Password reset for users\n- Session management\n- Ban/unban users\n\n## Quick Start\n\nAccount UI can be consumed in two ways:\n\n### Option A: Standalone Template\n\nClone the repo and own the source — edit anything you want:\n\n```bash\nnpx degit Kardoe-com/quickback-better-auth-account-ui my-account-app\ncd my-account-app\nnpm install\n```\n\nConfigure environment variables, build, and deploy:\n\n```bash\ncp .env.example .env.development\nnpm run build\nnpx wrangler deploy\n```\n\n### Option B: Library Import\n\nInstall as a dependency and get updates via `npm update`:\n\n```bash\nnpm install quickback-better-auth-account-ui\n```\n\n```tsx\n\nsetAppConfig({\n authRoute: 'quickback',\n name: 'My App',\n companyName: 'My Company',\n});\n```\n\nSee the [Library Usage](/account-ui/library-usage) guide for full details.\n\n### Which Mode Should I Use?\n\n| | Standalone | Library |\n|---|---|---|\n| **Install** | `npx degit` | `npm install` |\n| **Customize** | Edit source directly | `setAppConfig()` + CSS |\n| **Updates** | Manual (re-clone/merge) | `npm update` |\n| **Best for** | Full control, heavy customization | Embedding in existing apps, staying up to date |\n\n## Architecture\n\n### Standalone Mode\n\nThe Account UI runs as a **standalone SPA** that:\n\n1. **Runs separately** from your main application\n2. **Communicates** with your API backend via REST\n3. **Serves** from its own subdomain (e.g., `account.example.com`)\n4. **Redirects** users to your main app after authentication\n\n**Why standalone?** Security isolation, reusability across apps, edge performance via Cloudflare, and independent maintenance.\n\n### Library Mode\n\nWhen imported as a library, the Account UI becomes a **component in your existing React app**:\n\n1. **Renders inside** your application's router\n2. **Shares** your app's React and router context\n3. **Configured** via `setAppConfig()` — no env files needed\n4. **Updated** via `npm update` — no manual merging\n\n**Why library?** Seamless integration, automatic updates, and simpler deployment when you already have a React app.\n\n## Configuration\n\n### Standalone (Template)\n\nSince you own the source code, you can customize the Account UI by:\n\n1. **Environment variables** — Set `VITE_*` variables for build-time configuration\n2. **Edit source directly** — Modify `src/config/app.ts` for defaults, labels, messages, routes, and features\n3. **Runtime overrides** — Call `setAppConfig()` in `src/main.tsx` for programmatic configuration\n\n### Library\n\nWhen using as a library, configure entirely via `setAppConfig()`:\n\n```ts\n\nsetAppConfig({\n authRoute: 'quickback',\n name: 'My App',\n companyName: 'My Company',\n tagline: 'Build faster',\n});\n```\n\nSee the following guides for detailed configuration:\n\n- **[Environment Variables](/account-ui/environment-variables)** - Complete list of all configuration options\n- **[Feature Flags](/account-ui/features)** - Enable/disable features\n- **[Customization](/account-ui/customization)** - Branding, labels, and messaging\n- **[Worker Setup](/account-ui/worker)** - Cloudflare Worker configuration\n\n## Integration with Your App\n\n### Redirecting to Account UI\n\nFrom your main application, redirect users to the Account UI for authentication:\n\n```ts\n// Redirect to login\nwindow.location.href = 'https://account.example.com/login';\n\n// Redirect to profile\nwindow.location.href = 'https://account.example.com/profile';\n\n// Redirect to organization\nwindow.location.href = 'https://account.example.com/organizations/acme-corp';\n```\n\n### Returning to Your App\n\nAfter authentication, the Account UI can redirect users back to your application:\n\n```bash\n# Set the main app URL\nVITE_APP_URL=https://app.example.com\n\n# Optional: Set tenant URL pattern\nVITE_TENANT_URL_PATTERN=/organizations/{slug}\n```\n\nWhen a user logs in, they'll be redirected to `VITE_APP_URL`. When they access an organization, they'll be sent to the tenant URL (e.g., `https://app.example.com/organizations/acme-corp`).\n\n## API Integration\n\nThe Account UI connects to your API backend. The API path structure depends on your auth route mode:\n\n### Better Auth Mode (default)\n\n```\nhttps://api.example.com/api/auth/sign-in/email\nhttps://api.example.com/api/auth/sign-up/email\nhttps://api.example.com/api/auth/get-session\nhttps://api.example.com/api/auth/passkey/register\n```\n\n### Quickback Mode\n\n```\nhttps://api.example.com/auth/v1/sign-in/email\nhttps://api.example.com/auth/v1/sign-up/email\nhttps://api.example.com/auth/v1/get-session\nhttps://api.example.com/auth/v1/passkey/register\n```\n\nSee [Standalone Usage](/account-ui/standalone) for Better Auth backends or [With Quickback](/account-ui/with-quickback) for Quickback-compiled backends.\n\n## Project Structure\n\n```\nmy-account-app/\n├── src/\n│ ├── main.tsx # App entry point\n│ ├── App.tsx # Router & routes\n│ ├── worker.ts # Cloudflare Worker entry\n│ ├── auth/\n│ │ └── authClient.ts # Better Auth client\n│ ├── config/\n│ │ ├── app.ts # App config, labels, messages, features\n│ │ ├── features.ts # Feature flag helpers\n│ │ ├── routes.ts # Route helpers\n│ │ └── runtime.ts # Runtime API URL resolution\n│ ├── pages/ # Page components\n│ ├── components/ # Shared UI components\n│ ├── layouts/ # Auth & public layouts\n│ └── lib/ # API client, utilities\n├── .env.example # Environment template\n├── wrangler.toml # Cloudflare Worker config\n├── vite.config.ts # Vite build config\n└── tailwind.config.ts # Tailwind CSS config\n```\n\n## Next Steps\n\n- **[Standalone Usage](/account-ui/standalone)** - Use with any Better Auth backend (template mode)\n- **[With Quickback](/account-ui/with-quickback)** - Use with a Quickback-compiled backend\n- **[Library Usage](/account-ui/library-usage)** - Import as an npm package into your existing React app\n- **[Environment Variables](/account-ui/environment-variables)** - Configure your deployment\n- **[Feature Flags](/account-ui/features)** - Enable optional features\n- **[Customization](/account-ui/customization)** - Match your brand\n- **[Worker Setup](/account-ui/worker)** - Deploy to Cloudflare\n\n## See Also\n\n- [Stack Auth](/stack/auth) — Better Auth configuration, plugins, and security settings\n- [Compiler Auth Config](/compiler/config/providers#authentication) — Configure authentication in your Quickback project\n- [Quickback Stack](/stack) — The runtime infrastructure that Account UI connects to"
51
51
  },
52
52
  "account-ui/library-usage": {
53
53
  "title": "Library Usage",
@@ -67,7 +67,7 @@ export const DOCS = {
67
67
  },
68
68
  "changelog": {
69
69
  "title": "Changelog",
70
- "content": "# Changelog\n\nRelease notes for the Quickback compiler, CLI, and platform.\n\n---\n\n## v0.5.7 — February 12, 2026\n\n### Scoped Database for Actions\n\nActions now receive a **security-scoped database** instead of a raw Drizzle instance. The compiler generates a proxy wrapper that automatically enforces org isolation, owner filtering, and soft-delete visibility — the same protections that CRUD routes have always had.\n\nThe scoped DB uses duck-typed column detection at runtime:\n\n| Column Detected | SELECT / UPDATE / DELETE | INSERT |\n|---|---|---|\n| `organizationId` | Adds `WHERE organizationId = ?` | Auto-injects `organizationId` from context |\n| `ownerId` | Adds `WHERE ownerId = ?` | Auto-injects `ownerId` from context |\n| `deletedAt` | Adds `WHERE deletedAt IS NULL` | — |\n\nThis means **every action is secure by default** — no manual `WHERE` clauses needed.\n\n```typescript\ndefineActions(todos, {\n complete: {\n type: \"record\",\n execute: async ({ db, ctx, record, input }) => {\n // db is scoped — only sees records in user's org, excludes soft-deleted\n const siblings = await db.select().from(todos);\n // ↑ automatically filtered to ctx.activeOrgId + deletedAt IS NULL\n },\n },\n});\n```\n\n#### Unsafe Mode\n\nActions that intentionally need to bypass security (admin reports, cross-org queries, migrations) can declare `unsafe: true` to receive a raw, unscoped database handle:\n\n```typescript\ndefineActions(analytics, {\n globalReport: {\n unsafe: true,\n execute: async ({ db, rawDb, ctx, input }) => {\n // db → still scoped (safety net)\n // rawDb → bypasses all security filters\n const allOrgs = await rawDb.select().from(organizations);\n },\n },\n});\n```\n\nWithout `unsafe: true`, `rawDb` is `undefined`.\n\n**Related docs:** [Actions](/compiler/definitions/actions), [Actions API](/compiler/using-the-api/actions-api)\n\n---\n\n### Cascading Soft Delete\n\nSoft-deleting a parent record now **automatically cascades** to child and junction tables within the same feature. The compiler detects foreign key references at build time and generates cascade UPDATE statements.\n\n```\nDELETE /api/v1/projects/:id\n```\n\nGenerated behavior:\n```typescript\n// 1. Soft delete the parent\nawait db.update(projects)\n .set({ deletedAt: now, deletedBy: userId })\n .where(eq(projects.id, id));\n\n// 2. Auto-cascade to children (compiler-generated)\nawait db.update(projectMembers)\n .set({ deletedAt: now, deletedBy: userId })\n .where(eq(projectMembers.projectId, id));\n\nawait db.update(projectTasks)\n .set({ deletedAt: now, deletedBy: userId })\n .where(eq(projectTasks.projectId, id));\n```\n\nRules:\n- Only applies to **soft delete** (the default). Hard delete relies on database-level `ON DELETE CASCADE`.\n- Only cascades within the **same feature** — cross-feature references are not affected.\n- Child tables must have `deletedAt` / `deletedBy` columns (auto-added by the compiler's audit fields).\n\n**Related docs:** [Actions API — Cascading Soft Delete](/compiler/using-the-api/actions-api#cascading-soft-delete)\n\n---\n\n### Advanced Query Parameters\n\nNew query parameter capabilities for all list endpoints:\n\n- **Field selection** — `?fields=id,name,status` returns only the columns you need\n- **Multi-sort** — `?sort=status:asc,createdAt:desc` sorts by multiple fields\n- **Total count** — `?count=true` returns total matching records in response headers (`X-Total-Count`)\n- **Full-text search** — `?search=keyword` searches across all text columns\n\n```bash\n# Get only names and statuses, sorted by status then date, with total count\nGET /api/v1/todos?fields=id,name,status&sort=status:asc,createdAt:desc&count=true\n\n# Search across all text fields\nGET /api/v1/todos?search=urgent\n```\n\n**Related docs:** [Query Parameters](/compiler/using-the-api/query-params)\n\n---\n\n### Audit Field Improvements\n\n- `deletedAt` and `deletedBy` fields are now **always injected** by the compiler for tables with soft delete enabled — no need to define them in your schema\n- All audit fields (`createdAt`, `createdBy`, `modifiedAt`, `modifiedBy`, `deletedAt`, `deletedBy`) are auto-managed\n\n---\n\n## v0.5.6 — February 8, 2026\n\n### Database Naming Conventions\n\n- Default table and column naming changed to **snake_case** with `usePlurals: false`\n- Table names derived from generated Better Auth schema for consistency\n- Removed legacy single-database mode — split databases (auth + features) is now the standard\n\n### Auth Variable Shadowing Fix\n\n- Fixed `member` variable in auth middleware that shadowed the Drizzle `member` table import\n- Renamed to `sessionMember` to avoid conflicts in generated routes\n\n---\n\n## v0.5.5 — February 5, 2026\n\n### Better Auth Plugins\n\n- Published `@kardoe/better-auth-upgrade-anonymous` v1.1.0 — post-passkey email collection flow\n- Published `@kardoe/better-auth-combo-auth` — combined email + password + OTP authentication\n- Published `@kardoe/better-auth-aws-ses` — AWS SES email provider for Better Auth\n\n### OpenAPI Spec Generation\n\n- Generated APIs now include a full OpenAPI specification at `/openapi.json`\n- Better Auth endpoints included in the spec\n- Runtime route: `GET /openapi.json`\n\n### Security Hardening\n\n- Global error handler prevents leaking internal error details\n- Security headers middleware (CSP, HSTS, X-Frame-Options, X-Content-Type-Options)\n- `BETTER_AUTH_SECRET` properly passed to generated config\n\n---\n\n## v0.5.4 — January 30, 2026\n\n### Account UI\n\n- Pre-built authentication UI deployed as Cloudflare Workers\n- Features: sessions, organizations, passkeys, passwordless, admin panel, API keys\n- Dual-mode: standalone (degit template) or embedded with Quickback projects\n\n### Webhook System\n\n- Inbound webhook endpoints with signature verification\n- Outbound webhooks via Cloudflare Queues with automatic retries\n- Configurable per-feature webhook events\n\n### Realtime & Vector Search\n\n- Durable Objects + WebSocket realtime subscriptions\n- Vector embeddings via Cloudflare Vectorize\n- KV and R2 storage integrations\n\n---\n\n## v0.5.0 — January 2026\n\n### Initial Release\n\n- **Quickback Compiler** — TypeScript-first backend compiler\n- **Four Security Pillars** — Firewall, Access, Guards, Masking\n- **Combined Mode** — `defineTable()` with schema + security in a single file\n- **Templates** — Cloudflare Workers, Bun standalone, B2B SaaS\n- **Cloud Compiler** — Remote compilation via `compiler.quickback.dev`\n- **CLI** — `quickback create`, `quickback compile`, `quickback init`\n- **Better Auth Integration** — Organizations, roles, sessions\n- **Drizzle ORM** — Schema-first with automatic migrations\n- **Cloudflare D1** — Split database support (auth + features)"
70
+ "content": "# Changelog\n\nRelease notes for the Quickback compiler, CLI, and platform.\n\n---\n\n## v0.5.11 — February 24, 2026\n\n### Email OTP & Auth Fixes\n\n- Fixed email-OTP magic links not working correctly\n- Removed deprecated `/internal/validate` endpoint — use standard Better Auth session validation instead\n- Auth is now required for all API routes when running locally (previously some routes were unprotected in dev)\n\n### Multiple Table Exports Fix\n\n- Fixed a compiler error when a single file exports multiple Drizzle tables alongside a `defineTable()` default export\n- The CLI now properly detects and reports this with a clear error message pointing to the fix\n\n### Headless Drizzle Rename Hints\n\n- Added `compiler.migrations.renames` configuration for CI/CD environments where Drizzle's interactive rename prompts would block compilation\n- Compile errors now include explicit rename key paths and fail fast on malformed rename config keys\n- See [Configuration](/compiler/config) for details\n\n### Security Contract Report Artifacts\n\n- Added generated security contract artifacts to compiler output:\n - `reports/security-contracts.report.json`\n - `reports/security-contracts.report.sig.json`\n- Added config-driven signing controls:\n - `compiler.securityContracts.report.signature.enabled`\n - `compiler.securityContracts.report.signature.required`\n - `compiler.securityContracts.report.signature.key` / `keyEnv` / `keyId`\n- Missing required signing keys now fail loudly with explicit remediation guidance\n- Added strict config validation for report/signature paths and signing options\n\n### Mandatory Unsafe Action Audit Trail\n\n- Added structured unsafe action config (`unsafe: { reason, adminOnly, crossTenant, targetScope }`)\n- Cross-tenant unsafe actions now require Better Auth authentication plus platform admin role (`ctx.userRole === \"admin\"`)\n- Added mandatory audit logging for unsafe cross-tenant actions (success, denial, and error paths)\n- Cloudflare output now includes optional `AUDIT_DB` wiring, `drizzle.audit.config.ts`, and `db:migrate:audit:*` scripts when unsafe actions are present\n- Added compile-time raw SQL guard for actions and handlers (`allowRawSql: true` required per action)\n\n---\n\n## v0.5.10 — February 19, 2026\n\n### Compiler Page Parsing & CLI Output\n\n- Improved compiler page parsing for `definePage()` definitions\n- Better CLI output formatting during compilation\n- Added `apiPath` to schema registry for CMS integration\n\n### Bug Fixes\n\n- Fixed hyphenated action names not being properly quoted in generated code (e.g., `mark-complete` now generates valid JavaScript)\n- API key authentication (`x-api-key` header) is now handled separately from session tokens (`Bearer` header)\n\n---\n\n## v0.5.9 — February 16, 2026\n\n### CMS Pages & CLI Page Support\n\n- Added `definePage()` support for CMS-managed pages\n- Auth middleware improvements for page routes\n- CLI now supports page definitions alongside table and action definitions\n\n---\n\n## v0.5.8 — February 14, 2026\n\n### CMS App\n\n- Introduced the Quickback CMS — a schema-driven admin panel that connects to your generated API\n- CMS namespace added to actions for admin-specific operations\n- Fixed `guard` → `access` naming inconsistency in CMS action definitions\n\n### Schema Registry & Firewall Improvements\n\n- Added schema registry generator — the compiler now outputs a JSON schema registry used by the CMS\n- Firewall error modes: choose between `reveal` (403 with details) and `hide` (opaque 404) for security-sensitive deployments\n\n### Bug Fixes\n\n- Fixed anonymous user email format generation\n- Organization selector improvements in Account UI\n- Config validation now catches more errors at compile time\n- Better CRUD error handling with structured error responses\n- Fixed masking to use representative star counts instead of fixed formatting\n\n---\n\n## v0.5.7 — February 12, 2026\n\n### Scoped Database for Actions\n\nActions now receive a **security-scoped database** instead of a raw Drizzle instance. The compiler generates a proxy wrapper that automatically enforces org isolation, owner filtering, and soft-delete visibility — the same protections that CRUD routes have always had.\n\nThe scoped DB uses duck-typed column detection at runtime:\n\n| Column Detected | SELECT / UPDATE / DELETE | INSERT |\n|---|---|---|\n| `organizationId` | Adds `WHERE organizationId = ?` | Auto-injects `organizationId` from context |\n| `ownerId` | Adds `WHERE ownerId = ?` | Auto-injects `ownerId` from context |\n| `deletedAt` | Adds `WHERE deletedAt IS NULL` | — |\n\nThis means **every action is secure by default** — no manual `WHERE` clauses needed.\n\n```typescript\ndefineActions(todos, {\n complete: {\n type: \"record\",\n execute: async ({ db, ctx, record, input }) => {\n // db is scoped — only sees records in user's org, excludes soft-deleted\n const siblings = await db.select().from(todos);\n // ↑ automatically filtered to ctx.activeOrgId + deletedAt IS NULL\n },\n },\n});\n```\n\n#### Unsafe Mode\n\nActions that intentionally need to bypass security (admin reports, cross-org queries, migrations) can declare `unsafe: true` to receive a raw, unscoped database handle:\n\n```typescript\ndefineActions(analytics, {\n globalReport: {\n unsafe: true,\n execute: async ({ db, rawDb, ctx, input }) => {\n // db → still scoped (safety net)\n // rawDb → bypasses all security filters\n const allOrgs = await rawDb.select().from(organizations);\n },\n },\n});\n```\n\nWithout `unsafe: true`, `rawDb` is `undefined`.\n\n**Related docs:** [Actions](/compiler/definitions/actions), [Actions API](/compiler/using-the-api/actions-api)\n\n---\n\n### Cascading Soft Delete\n\nSoft-deleting a parent record now **automatically cascades** to child and junction tables within the same feature. The compiler detects foreign key references at build time and generates cascade UPDATE statements.\n\n```\nDELETE /api/v1/projects/:id\n```\n\nGenerated behavior:\n```typescript\n// 1. Soft delete the parent\nawait db.update(projects)\n .set({ deletedAt: now, deletedBy: userId })\n .where(eq(projects.id, id));\n\n// 2. Auto-cascade to children (compiler-generated)\nawait db.update(projectMembers)\n .set({ deletedAt: now, deletedBy: userId })\n .where(eq(projectMembers.projectId, id));\n\nawait db.update(projectTasks)\n .set({ deletedAt: now, deletedBy: userId })\n .where(eq(projectTasks.projectId, id));\n```\n\nRules:\n- Only applies to **soft delete** (the default). Hard delete relies on database-level `ON DELETE CASCADE`.\n- Only cascades within the **same feature** — cross-feature references are not affected.\n- Child tables must have `deletedAt` / `deletedBy` columns (auto-added by the compiler's audit fields).\n\n**Related docs:** [Actions API — Cascading Soft Delete](/compiler/using-the-api/actions-api#cascading-soft-delete)\n\n---\n\n### Advanced Query Parameters\n\nNew query parameter capabilities for all list endpoints:\n\n- **Field selection** — `?fields=id,name,status` returns only the columns you need\n- **Multi-sort** — `?sort=status:asc,createdAt:desc` sorts by multiple fields\n- **Total count** — `?count=true` returns total matching records in response headers (`X-Total-Count`)\n- **Full-text search** — `?search=keyword` searches across all text columns\n\n```bash\n# Get only names and statuses, sorted by status then date, with total count\nGET /api/v1/todos?fields=id,name,status&sort=status:asc,createdAt:desc&count=true\n\n# Search across all text fields\nGET /api/v1/todos?search=urgent\n```\n\n**Related docs:** [Query Parameters](/compiler/using-the-api/query-params)\n\n---\n\n### Audit Field Improvements\n\n- `deletedAt` and `deletedBy` fields are now **always injected** by the compiler for tables with soft delete enabled — no need to define them in your schema\n- All audit fields (`createdAt`, `createdBy`, `modifiedAt`, `modifiedBy`, `deletedAt`, `deletedBy`) are auto-managed\n\n---\n\n## v0.5.6 — February 8, 2026\n\n### Database Naming Conventions\n\n- Default table and column naming changed to **snake_case** with `usePlurals: false`\n- Table names derived from generated Better Auth schema for consistency\n- Removed legacy single-database mode — split databases (auth + features) is now the standard\n\n### Auth Variable Shadowing Fix\n\n- Fixed `member` variable in auth middleware that shadowed the Drizzle `member` table import\n- Renamed to `sessionMember` to avoid conflicts in generated routes\n\n---\n\n## v0.5.5 — February 5, 2026\n\n### Better Auth Plugins\n\n- Published `@kardoe/better-auth-upgrade-anonymous` v1.1.0 — post-passkey email collection flow\n- Published `@kardoe/better-auth-combo-auth` — combined email + password + OTP authentication\n- Published `@kardoe/better-auth-aws-ses` — AWS SES email provider for Better Auth\n\n### OpenAPI Spec Generation\n\n- Generated APIs now include a full OpenAPI specification at `/openapi.json`\n- Better Auth endpoints included in the spec\n- Runtime route: `GET /openapi.json`\n\n### Security Hardening\n\n- Global error handler prevents leaking internal error details\n- Security headers middleware (CSP, HSTS, X-Frame-Options, X-Content-Type-Options)\n- `BETTER_AUTH_SECRET` properly passed to generated config\n\n---\n\n## v0.5.4 — January 30, 2026\n\n### Account UI\n\n- Pre-built authentication UI deployed as Cloudflare Workers\n- Features: sessions, organizations, passkeys, passwordless, admin panel, API keys\n- Dual-mode: standalone (degit template) or embedded with Quickback projects\n\n### Webhook System\n\n- Inbound webhook endpoints with signature verification\n- Outbound webhooks via Cloudflare Queues with automatic retries\n- Configurable per-feature webhook events\n\n### Realtime & Vector Search\n\n- Durable Objects + WebSocket realtime subscriptions\n- Vector embeddings via Cloudflare Vectorize\n- KV and R2 storage integrations\n\n---\n\n## v0.5.0 — January 2026\n\n### Initial Release\n\n- **Quickback Compiler** — TypeScript-first backend compiler\n- **Four Security Pillars** — Firewall, Access, Guards, Masking\n- **Combined Mode** — `defineTable()` with schema + security in a single file\n- **Templates** — Cloudflare Workers, Bun standalone, B2B SaaS\n- **Cloud Compiler** — Remote compilation via `compiler.quickback.dev`\n- **CLI** — `quickback create`, `quickback compile`, `quickback init`\n- **Better Auth Integration** — Organizations, roles, sessions\n- **Drizzle ORM** — Schema-first with automatic migrations\n- **Cloudflare D1** — Split database support (auth + features)"
71
71
  },
72
72
  "cms/actions": {
73
73
  "title": "Actions",
@@ -91,11 +91,11 @@ export const DOCS = {
91
91
  },
92
92
  "cms/schema-format": {
93
93
  "title": "Schema Format Reference",
94
- "content": "# Schema Format Reference\n\nThe schema registry is a JSON file with a well-defined structure. Below are the complete TypeScript type definitions used by both the compiler (to generate) and the CMS (to consume).\n\n## SchemaRegistry\n\nThe top-level type:\n\n```typescript\ninterface SchemaRegistry {\n generatedAt: string;\n generatedBy: string;\n version: string;\n features: Record<string, string[]>;\n tables: Record<string, TableMeta>;\n tablesByFeature: Record<string, string[]>;\n}\n```\n\n| Field | Description |\n|-------|-------------|\n| `generatedAt` | ISO 8601 timestamp of when the registry was generated |\n| `generatedBy` | Always `\"quickback-compiler\"` |\n| `version` | Compiler version string |\n| `features` | Map of feature name to array of source file names |\n| `tables` | Map of camelCase table name to full table metadata |\n| `tablesByFeature` | Map of feature name to array of table names in that feature |\n\n## TableMeta\n\nFull metadata for a single table:\n\n```typescript\ninterface TableMeta {\n name: string;\n dbName: string;\n feature: string;\n columns: ColumnMeta[];\n firewall: Record<string, unknown>;\n crud: Record<string, CrudConfig>;\n guards: GuardsConfig;\n masking: Record<string, MaskingRule>;\n views: Record<string, ViewConfig>;\n validation: Record<string, ValidationRule>;\n actions: ActionMeta[];\n displayColumn?: string;\n internal?: boolean;\n}\n```\n\n| Field | Description |\n|-------|-------------|\n| `name` | camelCase table name (e.g., `\"accountCode\"`) |\n| `dbName` | Snake_case SQL table name (e.g., `\"account_code\"`) |\n| `feature` | Parent feature name (e.g., `\"accounting\"`) |\n| `columns` | Ordered array of column metadata |\n| `firewall` | Tenant isolation config (organization, owner, softDelete, exception) |\n| `crud` | Per-operation (create, read, update, delete) access config |\n| `guards` | Field-level create/update/immutable/protected rules |\n| `masking` | Per-field masking rules keyed by column name |\n| `views` | Named column projections keyed by view name |\n| `validation` | Per-field validation rules keyed by column name |\n| `actions` | Array of action definitions for this table |\n| `displayColumn` | Column used as human-readable label (auto-detected or explicit) |\n| `internal` | When `true`, table is hidden from CMS sidebar |\n\n## ColumnMeta\n\nMetadata for a single column:\n\n```typescript\ninterface ColumnMeta {\n name: string;\n dbName: string;\n type: \"text\" | \"integer\" | \"real\" | \"blob\";\n mode?: \"boolean\";\n primaryKey: boolean;\n notNull: boolean;\n defaultValue?: string | number | boolean;\n}\n```\n\n| Field | Description |\n|-------|-------------|\n| `name` | camelCase property name |\n| `dbName` | Snake_case SQL column name |\n| `type` | SQLite storage type |\n| `mode` | When `\"boolean\"`, an integer column represents true/false |\n| `primaryKey` | Whether this column is the primary key |\n| `notNull` | Whether the column has a NOT NULL constraint |\n| `defaultValue` | Static default value (strings, numbers, or booleans) |\n\n## CRUDConfig\n\nPer-operation access control:\n\n```typescript\ninterface CrudConfig {\n access?: AccessRule;\n mode?: string;\n}\n\ninterface AccessRule {\n roles?: string[];\n or?: Array<{\n roles?: string[];\n record?: Record<string, unknown>;\n }>;\n record?: Record<string, unknown>;\n}\n```\n\n| Field | Description |\n|-------|-------------|\n| `access.roles` | Array of roles allowed for this operation |\n| `access.or` | Alternative access conditions (any must match) |\n| `access.record` | Record-level conditions for access |\n| `mode` | Operation mode (e.g., `\"batch\"` for bulk create) |\n\n## GuardsConfig\n\nField-level control for create and update operations:\n\n```typescript\ninterface GuardsConfig {\n createable: string[];\n updatable: string[];\n immutable: string[];\n protected: Record<string, string[]>;\n}\n```\n\n| Field | Description |\n|-------|-------------|\n| `createable` | Fields that can be set during record creation |\n| `updatable` | Fields that can be modified on existing records |\n| `immutable` | Fields that can be set on create but never changed |\n| `protected` | Fields only modifiable via named actions (field name to action names) |\n\n## ActionMeta\n\nMetadata for a custom action:\n\n```typescript\ninterface ActionMeta {\n name: string;\n description: string;\n inputFields: ActionInputField[];\n access?: {\n roles: string[];\n record?: Record<string, unknown>;\n };\n standalone?: boolean;\n path?: string;\n method?: string;\n responseType?: string;\n sideEffects?: string;\n cms?: CmsConfig;\n}\n\ninterface CmsConfig {\n label?: string;\n icon?: string;\n confirm?: string | boolean;\n destructive?: boolean;\n category?: string;\n hidden?: boolean;\n successMessage?: string;\n onSuccess?: 'refresh' | 'redirect:list' | 'close';\n order?: number;\n}\n\ninterface ActionInputField {\n name: string;\n type: string;\n required: boolean;\n default?: unknown;\n}\n```\n\n| Field | Description |\n|-------|-------------|\n| `name` | Action identifier (e.g., `\"approve\"`, `\"applyPayment\"`) |\n| `description` | Human-readable description shown in dialog |\n| `inputFields` | Array of input field definitions |\n| `access.roles` | Roles allowed to execute this action |\n| `access.record` | Record conditions (e.g., `{ status: { equals: \"pending\" } }`) |\n| `standalone` | When `true`, action is not tied to a specific record |\n| `path` | Custom API path (overrides default) |\n| `method` | HTTP method (defaults to POST) |\n| `responseType` | `\"file\"` for download responses |\n| `sideEffects` | `\"sync\"` for actions with synchronous side effects |\n| `cms` | Optional CMS rendering metadata (label, icon, confirm, destructive, category, hidden, successMessage, onSuccess, order) |\n\n### ActionInputField\n\n| Field | Description |\n|-------|-------------|\n| `name` | Field identifier |\n| `type` | Zod type string: `\"string\"`, `\"number\"`, `\"boolean\"`, `\"array<string>\"` |\n| `required` | Whether the field must be provided |\n| `default` | Default value pre-filled in the form |\n\n## ViewConfig\n\nNamed column projection with access control:\n\n```typescript\ninterface ViewConfig {\n fields: string[];\n access: AccessRule;\n}\n```\n\n| Field | Description |\n|-------|-------------|\n| `fields` | Array of column names to include in this view |\n| `access` | Role-based access rules (same shape as CrudConfig access) |\n\n## MaskingRule\n\nPer-field data masking:\n\n```typescript\ninterface MaskingRule {\n type: \"email\" | \"phone\" | \"ssn\" | \"redact\";\n show: {\n roles: string[];\n or?: string;\n };\n}\n```\n\n| Field | Description |\n|-------|-------------|\n| `type` | Masking pattern to apply |\n| `show.roles` | Roles that see the unmasked value |\n| `show.or` | Alternative condition for showing unmasked value |\n\n### Masking Patterns\n\n| Type | Input | Output |\n|------|-------|--------|\n| `email` | `john@acme.com` | `j***@acme.com` |\n| `phone` | `(555) 123-4567` | `***-***-4567` |\n| `ssn` | `123-45-6789` | `***-**-6789` |\n| `redact` | Any string | `------` |\n\n## ValidationRule\n\nPer-field validation constraints:\n\n```typescript\ninterface ValidationRule {\n minLength?: number;\n maxLength?: number;\n min?: number;\n max?: number;\n enum?: string[];\n email?: boolean;\n}\n```\n\n| Field | Description |\n|-------|-------------|\n| `minLength` | Minimum string length |\n| `maxLength` | Maximum string length |\n| `min` | Minimum numeric value |\n| `max` | Maximum numeric value |\n| `enum` | Array of allowed string values |\n| `email` | When `true`, validates email format |\n\n## Next Steps\n\n- **[Schema Registry](/cms/schema-registry)** — How the registry is generated and used\n- **[Components Reference](/cms/components)** — All CMS React components"
94
+ "content": "# Schema Format Reference\n\nThe schema registry is a JSON file with a well-defined structure. Below are the complete TypeScript type definitions used by both the compiler (to generate) and the CMS (to consume).\n\n## SchemaRegistry\n\nThe top-level type:\n\n```typescript\ninterface SchemaRegistry {\n generatedAt: string;\n generatedBy: string;\n version: string;\n features: Record<string, string[]>;\n tables: Record<string, TableMeta>;\n tablesByFeature: Record<string, string[]>;\n}\n```\n\n| Field | Description |\n|-------|-------------|\n| `generatedAt` | ISO 8601 timestamp of when the registry was generated |\n| `generatedBy` | Always `\"quickback-compiler\"` |\n| `version` | Compiler version string |\n| `features` | Map of feature name to array of source file names |\n| `tables` | Map of camelCase table name to full table metadata |\n| `tablesByFeature` | Map of feature name to array of table names in that feature |\n\n## TableMeta\n\nFull metadata for a single table:\n\n```typescript\ninterface TableMeta {\n name: string;\n dbName: string;\n feature: string;\n columns: ColumnMeta[];\n firewall: Record<string, unknown>;\n crud: Record<string, CrudConfig>;\n guards: GuardsConfig;\n masking: Record<string, MaskingRule>;\n views: Record<string, ViewConfig>;\n validation: Record<string, ValidationRule>;\n actions: ActionMeta[];\n displayColumn?: string;\n inputHints?: Record<string, string>;\n internal?: boolean;\n}\n```\n\n| Field | Description |\n|-------|-------------|\n| `name` | camelCase table name (e.g., `\"accountCode\"`) |\n| `dbName` | Snake_case SQL table name (e.g., `\"account_code\"`) |\n| `feature` | Parent feature name (e.g., `\"accounting\"`) |\n| `columns` | Ordered array of column metadata |\n| `firewall` | Tenant isolation config (organization, owner, softDelete, exception) |\n| `crud` | Per-operation (create, read, update, delete) access config |\n| `guards` | Field-level create/update/immutable/protected rules |\n| `masking` | Per-field masking rules keyed by column name |\n| `views` | Named column projections keyed by view name |\n| `validation` | Per-field validation rules keyed by column name |\n| `actions` | Array of action definitions for this table |\n| `displayColumn` | Column used as human-readable label (auto-detected or explicit) |\n| `inputHints` | Map of column name to preferred CMS input type (e.g., `\"select\"`, `\"textarea\"`, `\"checkbox\"`) |\n| `internal` | When `true`, table is hidden from CMS sidebar |\n\n## ColumnMeta\n\nMetadata for a single column:\n\n```typescript\ninterface ColumnMeta {\n name: string;\n dbName: string;\n type: \"text\" | \"integer\" | \"real\" | \"blob\";\n mode?: \"boolean\";\n primaryKey: boolean;\n notNull: boolean;\n defaultValue?: string | number | boolean;\n fkTarget?: string;\n}\n```\n\n| Field | Description |\n|-------|-------------|\n| `name` | camelCase property name |\n| `dbName` | Snake_case SQL column name |\n| `type` | SQLite storage type |\n| `mode` | When `\"boolean\"`, an integer column represents true/false |\n| `primaryKey` | Whether this column is the primary key |\n| `notNull` | Whether the column has a NOT NULL constraint |\n| `defaultValue` | Static default value (strings, numbers, or booleans) |\n| `fkTarget` | Target table name for FK columns (e.g., `\"contact\"` for a `vendorId` column) |\n\n## CRUDConfig\n\nPer-operation access control:\n\n```typescript\ninterface CrudConfig {\n access?: AccessRule;\n mode?: string;\n}\n\ninterface AccessRule {\n roles?: string[];\n or?: Array<{\n roles?: string[];\n record?: Record<string, unknown>;\n }>;\n record?: Record<string, unknown>;\n}\n```\n\n| Field | Description |\n|-------|-------------|\n| `access.roles` | Array of roles allowed for this operation |\n| `access.or` | Alternative access conditions (any must match) |\n| `access.record` | Record-level conditions for access |\n| `mode` | Operation mode (e.g., `\"batch\"` for bulk create) |\n\n## GuardsConfig\n\nField-level control for create and update operations:\n\n```typescript\ninterface GuardsConfig {\n createable: string[];\n updatable: string[];\n immutable: string[];\n protected: Record<string, string[]>;\n}\n```\n\n| Field | Description |\n|-------|-------------|\n| `createable` | Fields that can be set during record creation |\n| `updatable` | Fields that can be modified on existing records |\n| `immutable` | Fields that can be set on create but never changed |\n| `protected` | Fields only modifiable via named actions (field name to action names) |\n\n## ActionMeta\n\nMetadata for a custom action:\n\n```typescript\ninterface ActionMeta {\n name: string;\n description: string;\n inputFields: ActionInputField[];\n access?: {\n roles: string[];\n record?: Record<string, unknown>;\n };\n standalone?: boolean;\n path?: string;\n method?: string;\n responseType?: string;\n sideEffects?: string;\n cms?: CmsConfig;\n}\n\ninterface CmsConfig {\n label?: string;\n icon?: string;\n confirm?: string | boolean;\n destructive?: boolean;\n category?: string;\n hidden?: boolean;\n successMessage?: string;\n onSuccess?: 'refresh' | 'redirect:list' | 'close';\n order?: number;\n}\n\ninterface ActionInputField {\n name: string;\n type: string;\n required: boolean;\n default?: unknown;\n}\n```\n\n| Field | Description |\n|-------|-------------|\n| `name` | Action identifier (e.g., `\"approve\"`, `\"applyPayment\"`) |\n| `description` | Human-readable description shown in dialog |\n| `inputFields` | Array of input field definitions |\n| `access.roles` | Roles allowed to execute this action |\n| `access.record` | Record conditions (e.g., `{ status: { equals: \"pending\" } }`) |\n| `standalone` | When `true`, action is not tied to a specific record |\n| `path` | Custom API path (overrides default) |\n| `method` | HTTP method (defaults to POST) |\n| `responseType` | `\"file\"` for download responses |\n| `sideEffects` | `\"sync\"` for actions with synchronous side effects |\n| `cms` | Optional CMS rendering metadata (label, icon, confirm, destructive, category, hidden, successMessage, onSuccess, order) |\n\n### ActionInputField\n\n| Field | Description |\n|-------|-------------|\n| `name` | Field identifier |\n| `type` | Zod type string: `\"string\"`, `\"number\"`, `\"boolean\"`, `\"array<string>\"` |\n| `required` | Whether the field must be provided |\n| `default` | Default value pre-filled in the form |\n\n## ViewConfig\n\nNamed column projection with access control:\n\n```typescript\ninterface ViewConfig {\n fields: string[];\n access: AccessRule;\n}\n```\n\n| Field | Description |\n|-------|-------------|\n| `fields` | Array of column names to include in this view |\n| `access` | Role-based access rules (same shape as CrudConfig access) |\n\n## MaskingRule\n\nPer-field data masking:\n\n```typescript\ninterface MaskingRule {\n type: \"email\" | \"phone\" | \"ssn\" | \"redact\";\n show: {\n roles: string[];\n or?: string;\n };\n}\n```\n\n| Field | Description |\n|-------|-------------|\n| `type` | Masking pattern to apply |\n| `show.roles` | Roles that see the unmasked value |\n| `show.or` | Alternative condition for showing unmasked value |\n\n### Masking Patterns\n\n| Type | Input | Output |\n|------|-------|--------|\n| `email` | `john@acme.com` | `j***@acme.com` |\n| `phone` | `(555) 123-4567` | `***-***-4567` |\n| `ssn` | `123-45-6789` | `***-**-6789` |\n| `redact` | Any string | `------` |\n\n## ValidationRule\n\nPer-field validation constraints:\n\n```typescript\ninterface ValidationRule {\n minLength?: number;\n maxLength?: number;\n min?: number;\n max?: number;\n enum?: string[];\n email?: boolean;\n}\n```\n\n| Field | Description |\n|-------|-------------|\n| `minLength` | Minimum string length |\n| `maxLength` | Maximum string length |\n| `min` | Minimum numeric value |\n| `max` | Maximum numeric value |\n| `enum` | Array of allowed string values |\n| `email` | When `true`, validates email format |\n\n## Next Steps\n\n- **[Schema Registry](/cms/schema-registry)** — How the registry is generated and used\n- **[Components Reference](/cms/components)** — All CMS React components"
95
95
  },
96
96
  "cms/schema-registry": {
97
97
  "title": "Schema Registry",
98
- "content": "# Schema Registry\n\nThe schema registry is a static JSON file generated by the Quickback compiler. It contains full metadata about every table in your project — columns, types, guards, masking rules, views, actions, validation, and firewall config. The CMS reads this file to render its entire UI.\n\n## Enabling Generation\n\nAdd `schemaRegistry` to your config:\n\n```typescript title=\"quickback/quickback.config.ts\"\nexport default defineConfig({\n schemaRegistry: { generate: true },\n // ... rest of your config\n});\n```\n\nRun `quickback compile` and the compiler outputs `schema-registry.json` alongside your compiled API files.\n\n## Output Shape\n\nThe top-level structure of `schema-registry.json`:\n\n```json\n{\n \"generatedAt\": \"2026-02-14T06:26:53.554Z\",\n \"generatedBy\": \"quickback-compiler\",\n \"version\": \"1.0.0\",\n \"features\": { ... },\n \"tables\": { ... },\n \"tablesByFeature\": { ... }\n}\n```\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `generatedAt` | `string` | ISO timestamp of generation |\n| `generatedBy` | `string` | Always `\"quickback-compiler\"` |\n| `version` | `string` | Compiler version used |\n| `features` | `Record<string, string[]>` | Feature name to file list mapping |\n| `tables` | `Record<string, TableMeta>` | Table name to full metadata |\n| `tablesByFeature` | `Record<string, string[]>` | Feature name to table name list |\n\n## TableMeta\n\nEach table entry contains everything the CMS needs to render its UI:\n\n```typescript\ninterface TableMeta {\n name: string; // camelCase table name (e.g., \"accountCode\")\n dbName: string; // SQL table name (e.g., \"account_code\")\n feature: string; // Parent feature name\n columns: ColumnMeta[]; // All columns including audit fields\n firewall: Record<string, unknown>; // Tenant isolation config\n crud: Record<string, CrudConfig>; // Per-operation access rules\n guards: {\n createable: string[]; // Fields allowed on create\n updatable: string[]; // Fields allowed on update\n immutable: string[]; // Fields locked after creation\n protected: Record<string, string[]>; // Fields only updatable via actions\n };\n masking: Record<string, MaskingRule>; // Per-field masking rules\n views: Record<string, ViewConfig>; // Named column projections\n validation: Record<string, ValidationRule>; // Per-field validation\n actions: ActionMeta[]; // Available actions for this table\n displayColumn?: string; // Human-readable label column\n internal?: boolean; // Hidden from CMS sidebar when true\n}\n```\n\n## ColumnMeta\n\nEach column in the `columns` array:\n\n```typescript\ninterface ColumnMeta {\n name: string; // Property name (camelCase)\n dbName: string; // SQL column name (snake_case)\n type: \"text\" | \"integer\" | \"real\" | \"blob\"; // SQLite type\n mode?: \"boolean\"; // When an integer represents a boolean\n primaryKey: boolean;\n notNull: boolean;\n defaultValue?: string | number | boolean;\n}\n```\n\nThe compiler automatically includes audit fields (`id`, `organizationId`, `createdAt`, `createdBy`, `modifiedAt`, `modifiedBy`, `deletedAt`) at the beginning of every table's column list.\n\n## Display Column\n\nThe `displayColumn` field tells the CMS which column to use as a human-readable label for a record. This is used in:\n\n- FK typeahead dropdowns (showing names instead of IDs)\n- Record titles in detail views\n- Breadcrumb labels\n\n### Auto-Detection\n\nIf you don't explicitly set `displayColumn` in your resource config, the compiler auto-detects it by scanning column names in priority order:\n\n1. `name`\n2. `title`\n3. `label`\n4. `headline`\n5. `subject`\n6. `code`\n7. `displayName`\n8. `fullName`\n9. `description`\n\nThe first match wins. If no candidate matches, the table has no display column and the CMS falls back to showing IDs.\n\n### Explicit Config\n\nSet it explicitly in your table definition:\n\n```typescript\nexport default defineTable(contacts, {\n displayColumn: \"companyName\",\n // ...\n});\n```\n\n## FK Label Resolution\n\nWhen a table has foreign key columns (ending in `Id`), the API enriches list responses with `_label` fields. For example, a `roomTypeId` column gets a corresponding `roomType_label` field containing the display column value from the referenced table.\n\nThe CMS uses these `_label` fields to show human-readable names in table cells and FK typeahead dropdowns instead of raw UUIDs.\n\n```\nroomTypeId: \"rt_abc123\" → displayed as \"Master Bedroom\"\naccountCodeId: \"ac_xyz789\" → displayed as \"4100 - Revenue\"\n```\n\nThe FK target table is derived by stripping the `Id` suffix: `roomTypeId` references the `roomType` table. The CMS confirms the target exists in the registry before rendering a link.\n\n## Next Steps\n\n- **[Connecting](/cms/connecting)** — Demo mode vs. live mode setup\n- **[Schema Format Reference](/cms/schema-format)** — Full TypeScript types"
98
+ "content": "# Schema Registry\n\nThe schema registry is a static JSON file generated by the Quickback compiler. It contains full metadata about every table in your project — columns, types, guards, masking rules, views, actions, validation, and firewall config. The CMS reads this file to render its entire UI.\n\n## Enabling Generation\n\nAdd `schemaRegistry` to your config:\n\n```typescript title=\"quickback/quickback.config.ts\"\nexport default defineConfig({\n schemaRegistry: { generate: true },\n // ... rest of your config\n});\n```\n\nRun `quickback compile` and the compiler outputs `schema-registry.json` alongside your compiled API files.\n\n## Output Shape\n\nThe top-level structure of `schema-registry.json`:\n\n```json\n{\n \"generatedAt\": \"2026-02-14T06:26:53.554Z\",\n \"generatedBy\": \"quickback-compiler\",\n \"version\": \"1.0.0\",\n \"features\": { ... },\n \"tables\": { ... },\n \"tablesByFeature\": { ... }\n}\n```\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `generatedAt` | `string` | ISO timestamp of generation |\n| `generatedBy` | `string` | Always `\"quickback-compiler\"` |\n| `version` | `string` | Compiler version used |\n| `features` | `Record<string, string[]>` | Feature name to file list mapping |\n| `tables` | `Record<string, TableMeta>` | Table name to full metadata |\n| `tablesByFeature` | `Record<string, string[]>` | Feature name to table name list |\n\n## TableMeta\n\nEach table entry contains everything the CMS needs to render its UI:\n\n```typescript\ninterface TableMeta {\n name: string; // camelCase table name (e.g., \"accountCode\")\n dbName: string; // SQL table name (e.g., \"account_code\")\n feature: string; // Parent feature name\n columns: ColumnMeta[]; // All columns including audit fields\n firewall: Record<string, unknown>; // Tenant isolation config\n crud: Record<string, CrudConfig>; // Per-operation access rules\n guards: {\n createable: string[]; // Fields allowed on create\n updatable: string[]; // Fields allowed on update\n immutable: string[]; // Fields locked after creation\n protected: Record<string, string[]>; // Fields only updatable via actions\n };\n masking: Record<string, MaskingRule>; // Per-field masking rules\n views: Record<string, ViewConfig>; // Named column projections\n validation: Record<string, ValidationRule>; // Per-field validation\n actions: ActionMeta[]; // Available actions for this table\n displayColumn?: string; // Human-readable label column\n internal?: boolean; // Hidden from CMS sidebar when true\n}\n```\n\n## ColumnMeta\n\nEach column in the `columns` array:\n\n```typescript\ninterface ColumnMeta {\n name: string; // Property name (camelCase)\n dbName: string; // SQL column name (snake_case)\n type: \"text\" | \"integer\" | \"real\" | \"blob\"; // SQLite type\n mode?: \"boolean\"; // When an integer represents a boolean\n primaryKey: boolean;\n notNull: boolean;\n defaultValue?: string | number | boolean;\n fkTarget?: string; // Target table name for FK columns\n}\n```\n\nThe compiler automatically includes audit fields (`id`, `organizationId`, `createdAt`, `createdBy`, `modifiedAt`, `modifiedBy`, `deletedAt`) at the beginning of every table's column list.\n\n### fkTarget — FK Resolution\n\nColumns ending in `Id` may have a `fkTarget` property indicating which table they reference. This is resolved in priority order:\n\n1. **Explicit `references`** — from your `defineTable()` config (highest priority)\n2. **Drizzle `.references()`** — parsed from schema source code\n3. **Convention** — strip `Id` suffix, match table name directly\n\n```json\n{\n \"name\": \"vendorId\",\n \"type\": \"text\",\n \"fkTarget\": \"contact\"\n}\n```\n\nThe CMS uses `fkTarget` to render typeahead/lookup inputs that search the correct table instead of showing raw IDs.\n\n## Input Hints\n\nTables with `inputHints` configured in `defineTable()` include an `inputHints` map in their metadata:\n\n```json\n{\n \"name\": \"invoice\",\n \"inputHints\": {\n \"status\": \"select\",\n \"sortOrder\": \"radio\",\n \"isPartialPaymentDisabled\": \"checkbox\",\n \"headerMessage\": \"textarea\"\n }\n}\n```\n\nThe CMS reads these hints to render the appropriate form control for each field. See [Input Hints](/compiler/definitions/schema#input-hints) for the full list of supported values.\n\n## Display Column\n\nThe `displayColumn` field tells the CMS which column to use as a human-readable label for a record. This is used in:\n\n- FK typeahead dropdowns (showing names instead of IDs)\n- Record titles in detail views\n- Breadcrumb labels\n\n### Auto-Detection\n\nIf you don't explicitly set `displayColumn` in your resource config, the compiler auto-detects it by scanning column names in priority order:\n\n1. `name`\n2. `title`\n3. `label`\n4. `headline`\n5. `subject`\n6. `code`\n7. `displayName`\n8. `fullName`\n9. `description`\n\nThe first match wins. If no candidate matches, the table has no display column and the CMS falls back to showing IDs.\n\n### Explicit Config\n\nSet it explicitly in your table definition:\n\n```typescript\nexport default defineTable(contacts, {\n displayColumn: \"companyName\",\n // ...\n});\n```\n\n## FK Label Resolution\n\nWhen a table has foreign key columns (ending in `Id`), the API enriches list responses with `_label` fields. For example, a `roomTypeId` column gets a corresponding `roomType_label` field containing the display column value from the referenced table.\n\nThe CMS uses these `_label` fields to show human-readable names in table cells and FK typeahead dropdowns instead of raw UUIDs.\n\n```\nroomTypeId: \"rt_abc123\" → displayed as \"Master Bedroom\"\naccountCodeId: \"ac_xyz789\" → displayed as \"4100 - Revenue\"\n```\n\nThe FK target table is resolved from the `fkTarget` property on each column. For simple cases like `roomTypeId` `roomType`, the compiler auto-detects it. For non-obvious mappings (e.g., `vendorId` `contact`), use explicit [`references`](/compiler/definitions/schema#references) in your `defineTable()` config.\n\n## Next Steps\n\n- **[Connecting](/cms/connecting)** — Demo mode vs. live mode setup\n- **[Schema Format Reference](/cms/schema-format)** — Full TypeScript types"
99
99
  },
100
100
  "cms/security": {
101
101
  "title": "Security",
@@ -111,7 +111,7 @@ export const DOCS = {
111
111
  },
112
112
  "compiler/cloud-compiler/cli": {
113
113
  "title": "CLI Reference",
114
- "content": "The Quickback CLI is the fastest way to create, compile, and manage your backend projects.\n\n## Installation\n\n```bash\nnpm install -g @kardoe/quickback\n```\n\n## Commands\n\n### Create a Project\n\n```bash\nquickback create <template> <name>\n```\n\n**Templates:**\n- `cloudflare` - Cloudflare Workers + D1 + Better Auth (free)\n- `bun` - Bun + SQLite + Better Auth (free)\n- `turso` - Turso/LibSQL + Better Auth (pro)\n\n**Example:**\n```bash\nquickback create cloudflare my-app\n```\n\nThis scaffolds a complete project with:\n- `quickback.config.ts` - Project configuration\n- `definitions/features/` - Your table definitions\n- Example todos feature with full security configuration\n\n### Compile Definitions\n\n```bash\nquickback compile\n```\n\nReads your definitions and generates:\n- Database migrations (Drizzle)\n- API route handlers (Hono)\n- TypeScript client SDK\n- OpenAPI specification\n\nRun this after making changes to your definitions.\n\n### Initialize Structure\n\n```bash\nquickback init\n```\n\nCreates the Quickback folder structure in an existing project:\n```\nquickback/\n definitions/\n features/\n auth/\n quickback.config.ts\n```\n\n### View Documentation\n\n```bash\nquickback docs # List available topics\nquickback docs <topic> # Show documentation for a topic\n```\n\n**Available topics:**\n- `firewall` - Data isolation layer\n- `access` - Role-based permissions\n- `guards` - Field protection\n- `masking` - PII redaction\n- `actions` - Custom business logic\n- `api` - CRUD endpoints reference\n- `config` - Configuration reference\n- `features` - Schema definitions\n\nDocumentation is bundled with the CLI and works offline.\n\n### Manage Claude Code Skill\n\n```bash\nquickback claude install # Interactive install\nquickback claude install --global # Install to ~/.claude/skills/\nquickback claude install --local # Install to ./quickback/.claude/\nquickback claude update # Update to latest version\nquickback claude remove # Remove installed skill\nquickback claude status # Check installation status\n```\n\nThe Quickback skill for Claude Code provides AI assistance for:\n- Creating resource definitions with proper security layers\n- Configuring Firewall, Access, Guards, and Masking\n- Debugging configuration issues\n- Understanding security patterns\n\n## Authentication\n\n### Login\n\n```bash\nquickback login\n```\n\nUses the [OAuth 2.0 Device Authorization Grant](https://datatracker.ietf.org/doc/html/rfc8628) (RFC 8628) to authenticate securely without exposing tokens in URLs.\n\n**How it works:**\n\n1. The CLI requests a one-time device code from the Quickback API.\n2. A code is displayed in your terminal (e.g., `AUL8-H93S`).\n3. Your browser opens to the Quickback account page where you approve the code.\n4. The CLI detects approval and exchanges it for a session token.\n5. If you belong to one organization, it's auto-selected. If you have multiple, you choose one.\n6. Credentials are stored locally.\n\n```\n$ quickback login\n\n🔐 Quickback Login\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n Your code: AUL8-H93S\n\n Visit: https://account.quickback.dev/cli/authorize\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n✓ Login successful!\nWelcome, Paul Stenhouse!\n\nUsing organization: Acme\n✓ Active organization: Acme\n```\n\nThis flow works in headless environments (SSH, containers, WSL) since it doesn't require a localhost callback.\n\n### Logout\n\n```bash\nquickback logout\n```\n\nClears stored credentials from `~/.quickback/credentials.json`.\n\n### Check Auth Status\n\n```bash\nquickback whoami\n```\n\nShows the currently authenticated user, organization, and token expiration.\n\n### Credential Storage\n\nCredentials are stored at `~/.quickback/credentials.json`:\n\n```json\n{\n \"token\": \"...\",\n \"user\": {\n \"id\": \"...\",\n \"email\": \"paul@example.com\",\n \"name\": \"Paul Stenhouse\",\n \"tier\": \"free\"\n },\n \"expiresAt\": \"2026-02-16T01:42:21.519Z\",\n \"organization\": {\n \"id\": \"...\",\n \"name\": \"Acme\",\n \"slug\": \"acme\"\n }\n}\n```\n\nSessions expire after 7 days. Run `quickback login` again to re-authenticate.\n\n### Organizations\n\nAfter login, the CLI auto-selects your organization:\n- **One organization** - automatically set as active.\n- **Multiple organizations** - you're prompted to choose one.\n\nThe active organization is stored in your credentials and sent with compile requests, so the compiler knows which org context to use.\n\n## Quick Start\n\n```bash\n# 1. Create a new project\nquickback create cloudflare my-app\ncd my-app\n\n# 2. Define your tables in definitions/features/\n# (Already has example todos feature)\n\n# 3. Compile\nquickback compile\n\n# 4. Run\nnpm run dev\n```\n\n## Options\n\n| Flag | Description |\n|------|-------------|\n| `-v, --version` | Show version number |\n| `-h, --help` | Show help message |\n\n## Environment Variables\n\n| Variable | Description |\n|----------|-------------|\n| `QUICKBACK_API_KEY` | API key for authentication (alternative to `quickback login`) |\n| `QUICKBACK_API_URL` | Override compiler API URL |\n\n### API Key Authentication\n\nUse an API key instead of interactive login. Useful for CI/CD pipelines and automated workflows:\n\n```bash\n# Pass API key for a single command\nQUICKBACK_API_KEY=your_api_key quickback compile\n\n# Or export for the session\nexport QUICKBACK_API_KEY=your_api_key\nquickback compile\n```\n\nThe API key takes precedence over stored credentials from `quickback login`.\n\nYou can create API keys from your [Quickback account](https://account.quickback.dev/profile). Each key is scoped to your organization.\n\n### Custom Compiler URL\n\nPoint the CLI to a different compiler (local or custom):\n\n```bash\n# Use a local compiler\nQUICKBACK_API_URL=http://localhost:3020 quickback compile\n\n# Or export for the session\nexport QUICKBACK_API_URL=http://localhost:3020\nquickback compile\n```\n\nSee [Local Compiler](/compiler/cloud-compiler/local-compiler) for running the compiler locally with Docker.\n\n## Troubleshooting\n\n### \"Command not found: quickback\"\n\nMake sure the CLI is installed globally:\n```bash\nnpm install -g @kardoe/quickback\n```\n\nOr use npx:\n```bash\nnpx @kardoe/quickback create cloudflare my-app\n```\n\n### Compile errors\n\n1. Check your `quickback.config.ts` exists and is valid\n2. Ensure all tables in `definitions/features/` have valid exports\n3. Run `quickback compile` with `--verbose` for detailed output\n\n### Authentication issues\n\nClear credentials and re-authenticate:\n```bash\nquickback logout\nquickback login\n```\n\n### \"Could not load organizations\"\n\nThis can happen if your session token expired or if the API is temporarily unavailable. Re-login:\n```bash\nquickback logout\nquickback login\n```"
114
+ "content": "The Quickback CLI is the fastest way to create, compile, and manage your backend projects.\n\n## Installation\n\n```bash\nnpm install -g @kardoe/quickback\n```\n\n## Commands\n\n### Create a Project\n\n```bash\nquickback create <template> <name>\n```\n\n**Templates:**\n- `cloudflare` - Cloudflare Workers + D1 + Better Auth (free)\n- `bun` - Bun + SQLite + Better Auth (free)\n- `turso` - Turso/LibSQL + Better Auth (pro)\n\n**Example:**\n```bash\nquickback create cloudflare my-app\n```\n\nThis scaffolds a complete project with:\n- `quickback.config.ts` - Project configuration\n- `quickback/features/` - Your table definitions\n- Example todos feature with full security configuration\n\n### Compile Definitions\n\n```bash\nquickback compile\n```\n\nReads your definitions and generates:\n- Database migrations (Drizzle)\n- API route handlers (Hono)\n- TypeScript client SDK\n- OpenAPI specification\n\nRun this after making changes to your definitions.\n\n### Initialize Structure\n\n```bash\nquickback init\n```\n\nCreates the Quickback folder structure in an existing project:\n```\nquickback/\n features/\n quickback.config.ts\n```\n\n### View Documentation\n\n```bash\nquickback docs # List available topics\nquickback docs <topic> # Show documentation for a topic\n```\n\n**Available topics:**\n- `firewall` - Data isolation layer\n- `access` - Role-based permissions\n- `guards` - Field protection\n- `masking` - PII redaction\n- `actions` - Custom business logic\n- `api` - CRUD endpoints reference\n- `config` - Configuration reference\n- `features` - Schema definitions\n\nDocumentation is bundled with the CLI and works offline.\n\n### Manage Claude Code Skill\n\n```bash\nquickback claude install # Interactive install\nquickback claude install --global # Install to ~/.claude/skills/\nquickback claude install --local # Install to ./quickback/.claude/\nquickback claude update # Update to latest version\nquickback claude remove # Remove installed skill\nquickback claude status # Check installation status\n```\n\nThe Quickback skill for Claude Code provides AI assistance for:\n- Creating resource definitions with proper security layers\n- Configuring Firewall, Access, Guards, and Masking\n- Debugging configuration issues\n- Understanding security patterns\n\n## Authentication\n\n### Login\n\n```bash\nquickback login\n```\n\nUses the [OAuth 2.0 Device Authorization Grant](https://datatracker.ietf.org/doc/html/rfc8628) (RFC 8628) to authenticate securely without exposing tokens in URLs.\n\n**How it works:**\n\n1. The CLI requests a one-time device code from the Quickback API.\n2. A code is displayed in your terminal (e.g., `AUL8-H93S`).\n3. Your browser opens to the Quickback account page where you approve the code.\n4. The CLI detects approval and exchanges it for a session token.\n5. If you belong to one organization, it's auto-selected. If you have multiple, you choose one.\n6. Credentials are stored locally.\n\n```\n$ quickback login\n\n🔐 Quickback Login\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n Your code: AUL8-H93S\n\n Visit: https://account.quickback.dev/cli/authorize\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n✓ Login successful!\nWelcome, Paul Stenhouse!\n\nUsing organization: Acme\n✓ Active organization: Acme\n```\n\nThis flow works in headless environments (SSH, containers, WSL) since it doesn't require a localhost callback.\n\n### Logout\n\n```bash\nquickback logout\n```\n\nClears stored credentials from `~/.quickback/credentials.json`.\n\n### Check Auth Status\n\n```bash\nquickback whoami\n```\n\nShows the currently authenticated user, organization, and token expiration.\n\n### Credential Storage\n\nCredentials are stored at `~/.quickback/credentials.json`:\n\n```json\n{\n \"token\": \"...\",\n \"user\": {\n \"id\": \"...\",\n \"email\": \"paul@example.com\",\n \"name\": \"Paul Stenhouse\",\n \"tier\": \"free\"\n },\n \"expiresAt\": \"2026-02-16T01:42:21.519Z\",\n \"organization\": {\n \"id\": \"...\",\n \"name\": \"Acme\",\n \"slug\": \"acme\"\n }\n}\n```\n\nSessions expire after 7 days. Run `quickback login` again to re-authenticate.\n\n### Organizations\n\nAfter login, the CLI auto-selects your organization:\n- **One organization** - automatically set as active.\n- **Multiple organizations** - you're prompted to choose one.\n\nThe active organization is stored in your credentials and sent with compile requests, so the compiler knows which org context to use.\n\n## Quick Start\n\n```bash\n# 1. Create a new project\nquickback create cloudflare my-app\ncd my-app\n\n# 2. Define your tables in quickback/features/\n# (Already has example todos feature)\n\n# 3. Compile\nquickback compile\n\n# 4. Run\nnpm run dev\n```\n\n## Options\n\n| Flag | Description |\n|------|-------------|\n| `-v, --version` | Show version number |\n| `-h, --help` | Show help message |\n\n## Environment Variables\n\n| Variable | Description |\n|----------|-------------|\n| `QUICKBACK_API_KEY` | API key for authentication (alternative to `quickback login`) |\n| `QUICKBACK_API_URL` | Override compiler API URL |\n\n### API Key Authentication\n\nUse an API key instead of interactive login. Useful for CI/CD pipelines and automated workflows:\n\n```bash\n# Pass API key for a single command\nQUICKBACK_API_KEY=your_api_key quickback compile\n\n# Or export for the session\nexport QUICKBACK_API_KEY=your_api_key\nquickback compile\n```\n\nThe API key takes precedence over stored credentials from `quickback login`.\n\nYou can create API keys from your [Quickback account](https://account.quickback.dev/profile). Each key is scoped to your organization.\n\n### Custom Compiler URL\n\nPoint the CLI to a different compiler (local or custom):\n\n```bash\n# Use a local compiler\nQUICKBACK_API_URL=http://localhost:3020 quickback compile\n\n# Or export for the session\nexport QUICKBACK_API_URL=http://localhost:3020\nquickback compile\n```\n\nSee [Local Compiler](/compiler/cloud-compiler/local-compiler) for running the compiler locally with Docker.\n\n## Troubleshooting\n\n### \"Command not found: quickback\"\n\nMake sure the CLI is installed globally:\n```bash\nnpm install -g @kardoe/quickback\n```\n\nOr use npx:\n```bash\nnpx @kardoe/quickback create cloudflare my-app\n```\n\n### Compile errors\n\n1. Check your `quickback.config.ts` exists and is valid\n2. Ensure all tables in `quickback/features/` have valid exports\n3. Run `quickback compile` with `--verbose` for detailed output\n4. For rename-related Drizzle failures in CI/headless runs, configure `compiler.migrations.renames` in `quickback.config.ts`\n\n### Authentication issues\n\nClear credentials and re-authenticate:\n```bash\nquickback logout\nquickback login\n```\n\n### \"Could not load organizations\"\n\nThis can happen if your session token expired or if the API is temporarily unavailable. Re-login:\n```bash\nquickback logout\nquickback login\n```"
115
115
  },
116
116
  "compiler/cloud-compiler/endpoints": {
117
117
  "title": "Endpoints",
@@ -119,7 +119,7 @@ export const DOCS = {
119
119
  },
120
120
  "compiler/cloud-compiler": {
121
121
  "title": "Cloud Compiler",
122
- "content": "Quickback offers a hosted compiler at `https://compiler.quickback.dev`. The CLI uses it to turn your definitions into a complete backend.\n\n## How It Works\n\n```\n┌──────────────┐ ┌──────────────────────┐\n│ quickback │ POST │ compiler.quickback │\n│ CLI │────────▶│ .dev/compile │\n│ │ │ │\n│ Sends: │ │ Returns: │\n│ - config │ │ - Hono routes │\n│ - features │◀────────│ - Drizzle migrations │\n│ - meta │ │ - TypeScript SDK │\n└──────────────┘ └──────────────────────┘\n```\n\n1. The CLI reads your `quickback.config.ts` and feature definitions.\n2. It sends them to the cloud compiler as a JSON payload.\n3. The compiler generates all backend files (routes, migrations, SDK, OpenAPI spec).\n4. The CLI writes the generated files to your project.\n\n## Quick Start\n\n```bash\n# 1. Install the CLI\nnpm install -g @kardoe/quickback\n\n# 2. Create a project\nquickback create cloudflare my-app\ncd my-app\n\n# 3. Log in\nquickback login\n\n# 4. Compile\nquickback compile\n```\n\n## Existing Databases\n\nWhen recompiling an existing project, the CLI automatically sends your Drizzle meta files so the compiler generates **incremental** migrations instead of fresh `CREATE TABLE` statements.\n\nMeta files are loaded from:\n- `drizzle/features/meta/` (dual database mode)\n- `drizzle/meta/` (single database mode)\n\nNo extra configuration needed — the CLI handles this automatically.\n\n## Next Steps\n\n- [CLI Reference](/compiler/cloud-compiler/cli) — All CLI commands\n- [Authentication](/compiler/cloud-compiler/authentication) — Login flow and API keys\n- [Endpoints](/compiler/cloud-compiler/endpoints) — Compiler API reference\n- [Troubleshooting](/compiler/cloud-compiler/troubleshooting) — Common issues\n- [Local Compiler](/compiler/cloud-compiler/local-compiler) — Run the compiler locally"
122
+ "content": "Quickback offers a hosted compiler at `https://compiler.quickback.dev`. The CLI uses it to turn your definitions into a complete backend.\n\n## How It Works\n\n```\n┌──────────────┐ ┌──────────────────────┐\n│ quickback │ POST │ compiler.quickback │\n│ CLI │────────▶│ .dev/compile │\n│ │ │ │\n│ Sends: │ │ Returns: │\n│ - config │ │ - Hono routes │\n│ - features │◀────────│ - Drizzle migrations │\n│ - meta │ │ - TypeScript SDK │\n└──────────────┘ └──────────────────────┘\n```\n\n1. The CLI reads your `quickback.config.ts` and feature definitions.\n2. It sends them to the cloud compiler as a JSON payload.\n3. The compiler generates all backend files (routes, migrations, SDK, OpenAPI spec).\n4. The CLI writes the generated files to your project.\n\n## Quick Start\n\n```bash\n# 1. Install the CLI\nnpm install -g @kardoe/quickback\n\n# 2. Create a project\nquickback create cloudflare my-app\ncd my-app\n\n# 3. Log in\nquickback login\n\n# 4. Compile\nquickback compile\n```\n\n## Existing Databases\n\nWhen recompiling an existing project, the CLI automatically sends your Drizzle meta files so the compiler generates **incremental** migrations instead of fresh `CREATE TABLE` statements.\n\nMeta files are loaded from:\n- `drizzle/auth/meta/`\n- `drizzle/features/meta/`\n- `drizzle/files/meta/`\n- `drizzle/webhooks/meta/`\n- `drizzle/meta/` (legacy single database mode)\n- `quickback/drizzle/...` mirrors (same paths under `quickback/`)\n\nAfter each compile, the CLI syncs generated meta JSON files back into `quickback/drizzle/...` (when a `quickback/` folder exists) so future compiles can always send the latest migration state.\n\nNo extra configuration needed — the CLI handles this automatically.\n\n## Next Steps\n\n- [CLI Reference](/compiler/cloud-compiler/cli) — All CLI commands\n- [Authentication](/compiler/cloud-compiler/authentication) — Login flow and API keys\n- [Endpoints](/compiler/cloud-compiler/endpoints) — Compiler API reference\n- [Troubleshooting](/compiler/cloud-compiler/troubleshooting) — Common issues\n- [Local Compiler](/compiler/cloud-compiler/local-compiler) — Run the compiler locally"
123
123
  },
124
124
  "compiler/cloud-compiler/local-compiler": {
125
125
  "title": "Local Compiler",
@@ -127,15 +127,15 @@ export const DOCS = {
127
127
  },
128
128
  "compiler/cloud-compiler/troubleshooting": {
129
129
  "title": "Troubleshooting",
130
- "content": "## 401 Unauthorized\n\nYour session may have expired (sessions last 7 days). Re-authenticate:\n\n```bash\nquickback logout\nquickback login\n```\n\n## Compilation timeout\n\nLarge projects may take longer to compile. The cloud compiler uses [Cloudflare Containers](https://developers.cloudflare.com/containers/) to run compilation in isolated environments. If you hit timeouts, try [running the compiler locally](/compiler/cloud-compiler/local-compiler).\n\n## \"Command not found: quickback\"\n\nMake sure the CLI is installed globally:\n```bash\nnpm install -g @kardoe/quickback\n```\n\nOr use npx:\n```bash\nnpx @kardoe/quickback create cloudflare my-app\n```\n\n## Compile errors\n\n1. Check your `quickback.config.ts` exists and is valid\n2. Ensure all tables in `definitions/features/` have valid exports\n3. Run `quickback compile` with `--verbose` for detailed output\n\n## \"Could not load organizations\"\n\nThis can happen if your session token expired or if the API is temporarily unavailable. Re-login:\n```bash\nquickback logout\nquickback login\n```"
130
+ "content": "## 401 Unauthorized\n\nYour session may have expired (sessions last 7 days). Re-authenticate:\n\n```bash\nquickback logout\nquickback login\n```\n\n## Compilation timeout\n\nLarge projects may take longer to compile. The cloud compiler uses [Cloudflare Containers](https://developers.cloudflare.com/containers/) to run compilation in isolated environments. If you hit timeouts, try [running the compiler locally](/compiler/cloud-compiler/local-compiler).\n\n## \"Command not found: quickback\"\n\nMake sure the CLI is installed globally:\n```bash\nnpm install -g @kardoe/quickback\n```\n\nOr use npx:\n```bash\nnpx @kardoe/quickback create cloudflare my-app\n```\n\n## Compile errors\n\n1. Check your `quickback.config.ts` exists and is valid\n2. Ensure all tables in `quickback/features/` have valid exports\n3. Run `quickback compile` with `--verbose` for detailed output\n\n## Drizzle rename prompts in CI/headless compile\n\nIf compilation fails with an interactive Drizzle message like:\n\n- `drizzle-kit requested interactive rename input, but Quickback compile is running headless`\n- `Missing rename hint for table/column ...`\n\nthen add explicit rename hints in `quickback.config.ts`:\n\n```typescript\nexport default defineConfig({\n // ...\n compiler: {\n migrations: {\n renames: {\n tables: {\n events_v2: \"events\",\n },\n columns: {\n events: {\n summary_text: \"summary\",\n },\n },\n },\n },\n },\n});\n```\n\n`tables` and `columns` mappings are always `new_name -> old_name`.\n\nIf a hint is missing, compile now fails loudly with the exact key path to add, for example:\n\n- `Expected hint key: compiler.migrations.renames.tables[\"events_v2\"]`\n- `Expected hint key: compiler.migrations.renames.columns[\"events\"][\"summary_text\"]`\n\n## \"Could not load organizations\"\n\nThis can happen if your session token expired or if the API is temporarily unavailable. Re-login:\n```bash\nquickback logout\nquickback login\n```"
131
131
  },
132
132
  "compiler/config": {
133
133
  "title": "Configuration",
134
- "content": "The `quickback.config.ts` file configures your Quickback project. It defines which providers to use, which features to enable, and how the compiler generates code.\n\n```typescript\n\nexport default defineConfig({\n name: \"my-app\",\n template: \"hono\",\n features: [\"organizations\"],\n providers: {\n runtime: defineRuntime(\"cloudflare\"),\n database: defineDatabase(\"cloudflare-d1\"),\n auth: defineAuth(\"better-auth\"),\n },\n});\n```\n\n## Required Fields\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `name` | `string` | Project name |\n| `template` | `\"hono\"` | Application template (`\"nextjs\"` is experimental) |\n| `providers` | `object` | Runtime, database, and auth provider configuration |\n\n## Optional Fields\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `features` | `string[]` | Feature flags, e.g. `[\"organizations\"]` |\n| `compiler` | `object` | Compiler options (audit fields, etc.) |\n| `openapi` | `object` | OpenAPI spec generation (`generate`, `publish`) — see [OpenAPI](/compiler/using-the-api/openapi) |\n\nSee the sub-pages for detailed reference on each section:\n- [Providers](/compiler/config/providers) — Runtime, database, and auth options\n- [Variables](/compiler/config/variables) — Environment variables and flags\n- [Output](/compiler/config/output) — Generated file structure"
134
+ "content": "The `quickback.config.ts` file configures your Quickback project. It defines which providers to use, which features to enable, and how the compiler generates code.\n\n```typescript\n\nexport default defineConfig({\n name: \"my-app\",\n template: \"hono\",\n features: [\"organizations\"],\n providers: {\n runtime: defineRuntime(\"cloudflare\"),\n database: defineDatabase(\"cloudflare-d1\"),\n auth: defineAuth(\"better-auth\"),\n },\n});\n```\n\n## Required Fields\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `name` | `string` | Project name |\n| `template` | `\"hono\"` | Application template (`\"nextjs\"` is experimental) |\n| `providers` | `object` | Runtime, database, and auth provider configuration |\n\n## Optional Fields\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `features` | `string[]` | Feature flags, e.g. `[\"organizations\"]` |\n| `compiler` | `object` | Compiler options (including migration rename hints for headless CI) |\n| `openapi` | `object` | OpenAPI spec generation (`generate`, `publish`) — see [OpenAPI](/compiler/using-the-api/openapi) |\n| `schemaRegistry` | `object` | Schema registry generation for the CMS (`{ generate: true }`) — see [Schema Registry](/cms/schema-registry) |\n\n## Headless Migration Rename Hints\n\nWhen `drizzle-kit generate` detects a potential rename, it normally prompts for confirmation. Cloud/CI compiles are non-interactive, so you must provide explicit rename hints.\n\nAdd hints in `compiler.migrations.renames`:\n\n```typescript\nexport default defineConfig({\n // ...\n compiler: {\n migrations: {\n renames: {\n // Keys are NEW names, values are OLD names\n tables: {\n events_v2: \"events\",\n },\n columns: {\n events: {\n summary_text: \"summary\",\n },\n },\n },\n },\n },\n});\n```\n\nRules:\n- `tables`: `new_table_name -> old_table_name`\n- `columns.<table>`: `new_column_name -> old_column_name`\n- Keys must match the names in your current schema (the new names)\n- Validation fails fast for malformed rename config, unsupported keys, or conflicting legacy/new rename paths.\n\n## Security Contract Reports and Signing\n\nAfter generation, Quickback verifies machine-checkable security contracts for Hono routes and RLS SQL.\n\nIt also emits a report artifact and signature artifact by default:\n\n- `reports/security-contracts.report.json`\n- `reports/security-contracts.report.sig.json`\n\nYou can configure output paths and signing behavior:\n\n```typescript\nexport default defineConfig({\n // ...\n compiler: {\n securityContracts: {\n failMode: \"error\", // or \"warn\"\n report: {\n path: \"reports/security-contracts.report.json\",\n signature: {\n enabled: true,\n required: true,\n keyEnv: \"QUICKBACK_SECURITY_REPORT_SIGNING_KEY\",\n keyId: \"prod-k1\",\n path: \"reports/security-contracts.report.sig.json\",\n },\n },\n },\n },\n});\n```\n\nIf `signature.required: true` and no key is available, compilation fails with a clear error message (or warning in `failMode: \"warn\"`).\n\nSee the sub-pages for detailed reference on each section:\n- [Providers](/compiler/config/providers) — Runtime, database, and auth options\n- [Variables](/compiler/config/variables) — Environment variables and flags\n- [Output](/compiler/config/output) — Generated file structure"
135
135
  },
136
136
  "compiler/config/output": {
137
137
  "title": "Output Structure",
138
- "content": "The compiler generates a complete project structure based on your definitions and provider configuration. The output varies depending on your runtime (Cloudflare vs Bun) and enabled features.\n\n**Warning:** Never edit files in `src/` directly. They are overwritten on every compile. Make changes in your `quickback/` definitions instead.\n\n## Cloudflare Output\n\n```\nsrc/\n├── index.ts # Hono app entry point (Workers export)\n├── env.d.ts # Cloudflare bindings TypeScript types\n├── db/\n│ ├── index.ts # Database connection factory\n│ ├── auth-schema.ts # Auth table schemas (dual mode)\n│ └── features-schema.ts # Feature table schemas (dual mode)\n├── auth/\n│ └── index.ts # Better Auth instance & config\n├── features/\n│ └── {feature}/\n│ ├── schema.ts # Drizzle table definition\n│ ├── routes.ts # CRUD + action endpoints\n│ └── actions.ts # Action handlers (if defined)\n├── lib/\n│ ├── access.ts # Access control helpers\n│ ├── types.ts # Runtime type definitions\n│ ├── masks.ts # Field masking utilities\n│ ├── services.ts # Service layer\n│ └── audit-wrapper.ts # Auto-inject audit fields\n└── middleware/\n ├── auth.ts # Auth context middleware\n ├── db.ts # Database instance middleware\n └── services.ts # Service injection middleware\n\ndrizzle/\n├── auth/ # Auth migrations (dual mode)\n│ ├── meta/\n│ │ ├── _journal.json\n│ │ └── 0000_snapshot.json\n│ └── 0000_initial.sql\n└── features/ # Feature migrations (dual mode)\n ├── meta/\n │ ├── _journal.json\n │ └── 0000_snapshot.json\n └── 0000_initial.sql\n\n# Root config files\n├── package.json\n├── tsconfig.json\n├── wrangler.toml # Cloudflare Workers config\n├── drizzle.config.ts # Features DB drizzle config\n└── drizzle.auth.config.ts # Auth DB drizzle config (dual mode)\n```\n\n## Bun Output\n\n```\nsrc/\n├── index.ts # Hono app entry point (Bun server)\n├── db/\n│ ├── index.ts # SQLite connection (bun:sqlite)\n│ └── schema.ts # Combined schema (single DB)\n├── auth/\n│ └── index.ts # Better Auth instance\n├── features/\n│ └── {feature}/\n│ ├── schema.ts\n│ ├── routes.ts\n│ └── actions.ts\n├── lib/\n│ ├── access.ts\n│ ├── types.ts\n│ ├── masks.ts\n│ ├── services.ts\n│ └── audit-wrapper.ts\n└── middleware/\n ├── auth.ts\n ├── db.ts\n └── services.ts\n\ndrizzle/ # Single migration directory\n├── meta/\n│ ├── _journal.json\n│ └── 0000_snapshot.json\n└── 0000_initial.sql\n\ndata/ # SQLite database files\n└── app.db\n\n├── package.json\n├── tsconfig.json\n└── drizzle.config.ts\n```\n\n## Key Differences by Runtime\n\n| Aspect | Cloudflare | Bun |\n|--------|-----------|-----|\n| Entry point | Workers `export default` | `Bun.serve()` with port |\n| Database | D1 bindings (dual mode) | SQLite file (single DB) |\n| Types | `env.d.ts` for bindings | No extra types needed |\n| Config | `wrangler.toml` | `.env` file |\n| Migrations | `drizzle/auth/` + `drizzle/features/` | `drizzle/` |\n| Drizzle configs | 2 configs (auth + features) | 1 config |\n\n## Optional Output Files\n\nThese files are generated only when the corresponding features are configured:\n\n### Embeddings\n\nWhen any feature has `embeddings` configured:\n\n```\nsrc/\n├── lib/\n│ └── embeddings.ts # Embedding helpers\n├── routes/\n│ └── embeddings.ts # POST /api/v1/embeddings endpoint\n└── queue-consumer.ts # Queue handler for async embedding jobs\n```\n\n### File Storage (R2)\n\nWhen `fileStorage` is configured:\n\n```\nsrc/\n└── routes/\n └── storage.ts # File upload/download endpoints\ndrizzle/\n└── files/ # File metadata migrations\n ├── meta/\n └── 0000_*.sql\n```\n\n### Webhooks\n\nWhen webhooks are enabled:\n\n```\nsrc/\n└── lib/\n └── webhooks/\n ├── index.ts # Webhook module entry\n ├── sign.ts # Webhook payload signing\n ├── handlers.ts # Handler registry\n ├── emit.ts # Queue emission helpers\n ├── routes.ts # Inbound/outbound endpoints\n └── providers/\n ├── index.ts\n └── stripe.ts # Stripe webhook handler\ndrizzle/\n└── webhooks/ # Webhook schema migrations\n ├── meta/\n └── 0000_*.sql\n```\n\n### Realtime\n\nWhen any feature has `realtime` configured:\n\n```\nsrc/\n└── lib/\n └── realtime.ts # Broadcast helpers\n\ncloudflare-workers/ # Separate Durable Objects worker\n└── broadcast/\n ├── Broadcaster.ts\n ├── index.ts\n └── wrangler.toml\n```\n\n### Device Authorization\n\nWhen the `deviceAuthorization` plugin is enabled:\n\n```\nsrc/\n└── routes/\n └── cli-auth.ts # Device auth flow endpoints\n```\n\n## Database Schemas\n\n### Dual Database Mode (Cloudflare Default)\n\nThe compiler separates schemas into two files:\n\n**`src/db/auth-schema.ts`** — Re-exports Better Auth table schemas:\n- `users`, `sessions`, `accounts`\n- `organizations`, `members`, `invitations` (if organizations enabled)\n- Plugin-specific tables (`apiKeys`, etc.)\n\n**`src/db/features-schema.ts`** — Re-exports your feature schemas:\n- All tables defined with `defineTable()`\n- Audit field columns added automatically\n\n### Single Database Mode (Bun Default)\n\n**`src/db/schema.ts`** — Combined re-export of all schemas (auth + features).\n\n## Generated Routes\n\nFor each feature, the compiler generates a routes file at `src/features/{name}/routes.ts` containing:\n\n| Route | Generated When |\n|-------|----------------|\n| `GET /` | `crud.list` configured |\n| `GET /:id` | `crud.get` configured |\n| `POST /` | `crud.create` configured |\n| `PATCH /:id` | `crud.update` configured |\n| `DELETE /:id` | `crud.delete` configured |\n| `PUT /:id` | `crud.put` configured |\n| `POST /batch` | `crud.create` exists (auto-enabled) |\n| `PATCH /batch` | `crud.update` exists (auto-enabled) |\n| `DELETE /batch` | `crud.delete` exists (auto-enabled) |\n| `PUT /batch` | `crud.put` exists (auto-enabled) |\n| `GET /views/{name}` | `views` configured |\n| `POST /:id/{action}` | Record-based actions defined |\n| `POST /{action}` | Standalone actions defined |\n\nAll routes are mounted under `/api/v1/{feature}` in the main app.\n\n## Migrations\n\nThe compiler runs `drizzle-kit generate` during compilation to produce SQL migration files. On subsequent compiles, it uses `existingFiles` (your current migration state) to generate only incremental changes.\n\nMigration files follow the Drizzle Kit naming convention:\n```\n0000_initial.sql\n0001_add_status_column.sql\n0002_create_orders_table.sql\n```\n\n## See Also\n\n- [Providers](/compiler/config/providers) — Configure runtime, database, and auth providers\n- [Environment variables](/compiler/config/variables) — Required variables by runtime"
138
+ "content": "The compiler generates a complete project structure based on your definitions and provider configuration. The output varies depending on your runtime (Cloudflare vs Bun) and enabled features.\n\n**Warning:** Never edit files in `src/` directly. They are overwritten on every compile. Make changes in your `quickback/` definitions instead.\n\n## Cloudflare Output\n\n```\nsrc/\n├── index.ts # Hono app entry point (Workers export)\n├── env.d.ts # Cloudflare bindings TypeScript types\n├── db/\n│ ├── index.ts # Database connection factory\n│ ├── auth-schema.ts # Auth table schemas (dual mode)\n│ └── features-schema.ts # Feature table schemas (dual mode)\n├── auth/\n│ └── index.ts # Better Auth instance & config\n├── features/\n│ └── {feature}/\n│ ├── schema.ts # Drizzle table definition\n│ ├── routes.ts # CRUD + action endpoints\n│ └── actions.ts # Action handlers (if defined)\n├── lib/\n│ ├── access.ts # Access control helpers\n│ ├── types.ts # Runtime type definitions\n│ ├── masks.ts # Field masking utilities\n│ ├── services.ts # Service layer\n│ ├── audit-wrapper.ts # Auto-inject audit fields\n└── security-audit.ts # Unsafe cross-tenant audit logger (when needed)\n└── middleware/\n ├── auth.ts # Auth context middleware\n ├── db.ts # Database instance middleware\n └── services.ts # Service injection middleware\n\ndrizzle/\n├── auth/ # Auth migrations (dual mode)\n│ ├── meta/\n│ │ ├── _journal.json\n│ │ └── 0000_snapshot.json\n│ └── 0000_initial.sql\n├── features/ # Feature migrations (dual mode)\n├── meta/\n│ ├── _journal.json\n│ └── 0000_snapshot.json\n│ └── 0000_initial.sql\n└── audit/ # Unsafe cross-tenant action audit migrations (when needed)\n ├── meta/\n └── 0000_initial.sql\n\n# Root config files\n├── package.json\n├── tsconfig.json\n├── wrangler.toml # Cloudflare Workers config\n├── drizzle.config.ts # Features DB drizzle config\n├── drizzle.auth.config.ts # Auth DB drizzle config (dual mode)\n└── drizzle.audit.config.ts # Audit DB drizzle config (when unsafe actions exist)\n```\n\n## Bun Output\n\n```\nsrc/\n├── index.ts # Hono app entry point (Bun server)\n├── db/\n│ ├── index.ts # SQLite connection (bun:sqlite)\n│ └── schema.ts # Combined schema (single DB)\n├── auth/\n│ └── index.ts # Better Auth instance\n├── features/\n│ └── {feature}/\n│ ├── schema.ts\n│ ├── routes.ts\n│ └── actions.ts\n├── lib/\n│ ├── access.ts\n│ ├── types.ts\n│ ├── masks.ts\n│ ├── services.ts\n│ └── audit-wrapper.ts\n└── middleware/\n ├── auth.ts\n ├── db.ts\n └── services.ts\n\ndrizzle/ # Single migration directory\n├── meta/\n│ ├── _journal.json\n│ └── 0000_snapshot.json\n└── 0000_initial.sql\n\ndata/ # SQLite database files\n└── app.db\n\n├── package.json\n├── tsconfig.json\n└── drizzle.config.ts\n```\n\n## Key Differences by Runtime\n\n| Aspect | Cloudflare | Bun |\n|--------|-----------|-----|\n| Entry point | Workers `export default` | `Bun.serve()` with port |\n| Database | D1 bindings (dual mode) | SQLite file (single DB) |\n| Types | `env.d.ts` for bindings | No extra types needed |\n| Config | `wrangler.toml` | `.env` file |\n| Migrations | `drizzle/auth/` + `drizzle/features/` | `drizzle/` |\n| Drizzle configs | 2 configs (auth + features) | 1 config |\n\n## Optional Output Files\n\nThese files are generated only when the corresponding features are configured:\n\n### Embeddings\n\nWhen any feature has `embeddings` configured:\n\n```\nsrc/\n├── lib/\n│ └── embeddings.ts # Embedding helpers\n├── routes/\n│ └── embeddings.ts # POST /api/v1/embeddings endpoint\n└── queue-consumer.ts # Queue handler for async embedding jobs\n```\n\n### File Storage (R2)\n\nWhen `fileStorage` is configured:\n\n```\nsrc/\n└── routes/\n └── storage.ts # File upload/download endpoints\ndrizzle/\n└── files/ # File metadata migrations\n ├── meta/\n └── 0000_*.sql\n```\n\n### Webhooks\n\nWhen webhooks are enabled:\n\n```\nsrc/\n└── lib/\n └── webhooks/\n ├── index.ts # Webhook module entry\n ├── sign.ts # Webhook payload signing\n ├── handlers.ts # Handler registry\n ├── emit.ts # Queue emission helpers\n ├── routes.ts # Inbound/outbound endpoints\n └── providers/\n ├── index.ts\n └── stripe.ts # Stripe webhook handler\ndrizzle/\n└── webhooks/ # Webhook schema migrations\n ├── meta/\n └── 0000_*.sql\n```\n\n### Security Audit Database (Unsafe Actions)\n\nWhen any action enables unsafe cross-tenant mode (`unsafe.crossTenant: true`):\n\n```\nsrc/\n├── db/\n│ └── audit-schema.ts # audit_events table\n└── lib/\n └── security-audit.ts # mandatory audit writer\n\ndrizzle/\n└── audit/\n ├── meta/\n └── 0000_*.sql\n\n# Root\n└── drizzle.audit.config.ts\n```\n\nCloudflare output also includes an `AUDIT_DB` D1 binding in `wrangler.toml` and migration scripts:\n\n- `db:migrate:audit:local`\n- `db:migrate:audit:remote`\n\n### Security Contract Report and Signature\n\nGenerated on every compile (unless disabled via `compiler.securityContracts.report.enabled: false`):\n\n```\nreports/\n├── security-contracts.report.json # Contract evaluation summary + violations\n└── security-contracts.report.sig.json # Signature / digest envelope for the report\n```\n\nThe signature file uses HMAC-SHA256 when a signing key is configured, otherwise it falls back to SHA-256 digest mode. \nSet `compiler.securityContracts.report.signature.required: true` to fail compilation when a signing key is missing.\n\n### Realtime\n\nWhen any feature has `realtime` configured:\n\n```\nsrc/\n└── lib/\n └── realtime.ts # Broadcast helpers\n\ncloudflare-workers/ # Separate Durable Objects worker\n└── broadcast/\n ├── Broadcaster.ts\n ├── index.ts\n └── wrangler.toml\n```\n\n### Device Authorization\n\nWhen the `deviceAuthorization` plugin is enabled:\n\n```\nsrc/\n└── routes/\n └── cli-auth.ts # Device auth flow endpoints\n```\n\n## Database Schemas\n\n### Dual Database Mode (Cloudflare Default)\n\nThe compiler separates schemas into two files:\n\n**`src/db/auth-schema.ts`** — Re-exports Better Auth table schemas:\n- `users`, `sessions`, `accounts`\n- `organizations`, `members`, `invitations` (if organizations enabled)\n- Plugin-specific tables (`apiKeys`, etc.)\n\n**`src/db/features-schema.ts`** — Re-exports your feature schemas:\n- All tables defined with `defineTable()`\n- Audit field columns added automatically\n\n### Single Database Mode (Bun Default)\n\n**`src/db/schema.ts`** — Combined re-export of all schemas (auth + features).\n\n## Generated Routes\n\nFor each feature, the compiler generates a routes file at `src/features/{name}/routes.ts` containing:\n\n| Route | Generated When |\n|-------|----------------|\n| `GET /` | `crud.list` configured |\n| `GET /:id` | `crud.get` configured |\n| `POST /` | `crud.create` configured |\n| `PATCH /:id` | `crud.update` configured |\n| `DELETE /:id` | `crud.delete` configured |\n| `PUT /:id` | `crud.put` configured |\n| `POST /batch` | `crud.create` exists (auto-enabled) |\n| `PATCH /batch` | `crud.update` exists (auto-enabled) |\n| `DELETE /batch` | `crud.delete` exists (auto-enabled) |\n| `PUT /batch` | `crud.put` exists (auto-enabled) |\n| `GET /views/{name}` | `views` configured |\n| `POST /:id/{action}` | Record-based actions defined |\n| `POST /{action}` | Standalone actions defined |\n\nAll routes are mounted under `/api/v1/{feature}` in the main app.\n\n## Migrations\n\nThe compiler runs `drizzle-kit generate` during compilation to produce SQL migration files. On subsequent compiles, it uses `existingFiles` (your current migration state) to generate only incremental changes.\n\nThe CLI loads migration meta JSON from:\n\n- `drizzle/auth/meta/`\n- `drizzle/features/meta/`\n- `drizzle/files/meta/`\n- `drizzle/webhooks/meta/`\n- `drizzle/meta/` (legacy single database mode)\n- `quickback/drizzle/...` mirrors (same paths under `quickback/`)\n\nAfter compile, the CLI syncs generated meta JSON back into `quickback/drizzle/...` (when a `quickback/` folder exists) so repeated compiles keep incremental migration state in your definitions area.\n\nFor non-interactive environments (cloud compile, CI), table/column renames must be declared with `compiler.migrations.renames` in `quickback.config.ts`. This avoids interactive rename prompts during migration generation.\n\nMigration files follow the Drizzle Kit naming convention:\n```\n0000_initial.sql\n0001_add_status_column.sql\n0002_create_orders_table.sql\n```\n\n## See Also\n\n- [Providers](/compiler/config/providers) — Configure runtime, database, and auth providers\n- [Environment variables](/compiler/config/variables) — Required variables by runtime"
139
139
  },
140
140
  "compiler/config/providers": {
141
141
  "title": "Providers",
@@ -143,59 +143,67 @@ export const DOCS = {
143
143
  },
144
144
  "compiler/config/variables": {
145
145
  "title": "Environment Variables",
146
- "content": "## CLI Environment Variables\n\nThese variables configure the Quickback CLI itself.\n\n| Variable | Description | Default |\n|----------|-------------|---------|\n| `QUICKBACK_API_URL` | Compiler API endpoint | `https://compiler.quickback.dev` |\n| `QUICKBACK_API_KEY` | API key for headless authentication (CI/CD) | — |\n| `QUICKBACK_AUTH_URL` | Auth server URL (custom deployments) | — |\n\n### Authentication\n\nThe CLI authenticates via two methods:\n\n1. **Interactive login** — `quickback login` stores credentials in `~/.quickback/credentials.json`\n2. **API key** — Set `QUICKBACK_API_KEY` for CI/CD environments\n\n```bash\n# Use the cloud compiler (default)\nquickback compile\n\n# Use a local compiler instance\nQUICKBACK_API_URL=http://localhost:3000 quickback compile\n\n# CI/CD with API key\nQUICKBACK_API_KEY=qb_key_... quickback compile\n```\n\n## Cloudflare Variables\n\n### Wrangler Bindings\n\nThese are configured as bindings in `wrangler.toml`, not environment variables. The compiler generates them automatically.\n\n| Binding | Type | Description |\n|---------|------|-------------|\n| `AUTH_DB` | D1 Database | Better Auth tables (dual mode) |\n| `DB` | D1 Database | Feature tables (dual mode) |\n| `DATABASE` | D1 Database | All tables (single DB mode) |\n| `KV` | KV Namespace | Key-value storage |\n| `R2_BUCKET` | R2 Bucket | File storage (if configured) |\n| `AI` | Workers AI | Embedding generation (if configured) |\n| `VECTORIZE` | Vectorize | Vector similarity search (if configured) |\n| `EMBEDDINGS_QUEUE` | Queue | Async embedding jobs (if configured) |\n| `WEBHOOKS_DB` | D1 Database | Webhook events (if configured) |\n| `WEBHOOKS_QUEUE` | Queue | Webhook delivery (if configured) |\n| `FILES_DB` | D1 Database | File metadata (if R2 configured) |\n| `BROADCASTER` | Service Binding | Realtime broadcast worker (if configured) |\n\n### Worker Variables\n\nSet these in `wrangler.toml` under `[vars]` or in the Cloudflare dashboard:\n\n| Variable | Description | Required |\n|----------|-------------|----------|\n| `BETTER_AUTH_URL` | Public URL of your auth endpoint | Yes |\n| `APP_NAME` | Application name (used in emails) | No |\n\n### Email (AWS SES)\n\nRequired when using the `emailOtp` plugin with AWS SES:\n\n| Variable | Description |\n|----------|-------------|\n| `AWS_ACCESS_KEY_ID` | AWS access key |\n| `AWS_SECRET_ACCESS_KEY` | AWS secret key |\n| `AWS_SES_REGION` | SES region (e.g., `us-east-2`) |\n| `EMAIL_FROM` | Sender email address |\n| `EMAIL_FROM_NAME` | Sender display name |\n| `EMAIL_REPLY_TO` | Reply-to address |\n\n### Drizzle Kit (Migrations)\n\nFor running remote migrations with `drizzle-kit`, set these in `.env`:\n\n| Variable | Description |\n|----------|-------------|\n| `CLOUDFLARE_ACCOUNT_ID` | Your Cloudflare account ID |\n| `CLOUDFLARE_API_TOKEN` | API token with D1 permissions |\n| `CLOUDFLARE_AUTH_DATABASE_ID` | Auth D1 database ID (dual mode) |\n| `CLOUDFLARE_FEATURES_DATABASE_ID` | Features D1 database ID (dual mode) |\n| `CLOUDFLARE_DATABASE_ID` | Database ID (single DB mode) |\n\n## Bun Variables\n\nSet these in a `.env` file in your project root:\n\n| Variable | Description | Default |\n|----------|-------------|---------|\n| `NODE_ENV` | Runtime environment | `development` |\n| `PORT` | Server port | `3000` |\n| `BETTER_AUTH_SECRET` | Auth encryption secret | — (required) |\n| `BETTER_AUTH_URL` | Public URL of your server | `http://localhost:3000` |\n| `DATABASE_PATH` | Path to SQLite file | `./data/app.db` |\n\n## Turso (LibSQL) Variables\n\nIn addition to the Bun variables above:\n\n| Variable | Description | Default |\n|----------|-------------|---------|\n| `DATABASE_URL` | LibSQL connection URL | `file:./data/app.db` |\n| `DATABASE_AUTH_TOKEN` | Turso auth token (required for remote) | — |\n\n```bash\n# Local development\nDATABASE_URL=file:./data/app.db\n\n# Production (Turso cloud)\nDATABASE_URL=libsql://your-db-slug.turso.io\nDATABASE_AUTH_TOKEN=eyJhbGciOi...\n```\n\n## Social Login Providers\n\nWhen social login is configured in your auth provider:\n\n| Variable | Description |\n|----------|-------------|\n| `GOOGLE_CLIENT_ID` | Google OAuth client ID |\n| `GOOGLE_CLIENT_SECRET` | Google OAuth client secret |\n| `GITHUB_CLIENT_ID` | GitHub OAuth client ID |\n| `GITHUB_CLIENT_SECRET` | GitHub OAuth client secret |\n| `DISCORD_CLIENT_ID` | Discord OAuth client ID |\n| `DISCORD_CLIENT_SECRET` | Discord OAuth client secret |\n\n## See Also\n\n- [Output Structure](/compiler/config/output) — Generated file structure\n- [Providers](/compiler/config/providers) — Provider configuration reference\n- [Cloudflare Template](/compiler/getting-started/template-cloudflare) — Cloudflare setup guide\n- [Bun Template](/compiler/getting-started/template-bun) — Bun setup guide"
146
+ "content": "## CLI Environment Variables\n\nThese variables configure the Quickback CLI itself.\n\n| Variable | Description | Default |\n|----------|-------------|---------|\n| `QUICKBACK_API_URL` | Compiler API endpoint | `https://compiler.quickback.dev` |\n| `QUICKBACK_API_KEY` | API key for headless authentication (CI/CD) | — |\n| `QUICKBACK_AUTH_URL` | Auth server URL (custom deployments) | — |\n\n### Authentication\n\nThe CLI authenticates via two methods:\n\n1. **Interactive login** — `quickback login` stores credentials in `~/.quickback/credentials.json`\n2. **API key** — Set `QUICKBACK_API_KEY` for CI/CD environments\n\n```bash\n# Use the cloud compiler (default)\nquickback compile\n\n# Use a local compiler instance\nQUICKBACK_API_URL=http://localhost:3000 quickback compile\n\n# CI/CD with API key\nQUICKBACK_API_KEY=qb_key_... quickback compile\n```\n\n## Compiler Service Variables\n\nThese variables are used by the compiler service/runtime itself.\n\n| Variable | Description | Default |\n|----------|-------------|---------|\n| `QUICKBACK_SECURITY_REPORT_SIGNING_KEY` | HMAC key for signing `security-contracts.report.json` artifacts | — |\n\nWhen `compiler.securityContracts.report.signature.required` is `true`, this variable (or `signature.key`) must be set or compile fails.\n\n## Cloudflare Variables\n\n### Wrangler Bindings\n\nThese are configured as bindings in `wrangler.toml`, not environment variables. The compiler generates them automatically.\n\n| Binding | Type | Description |\n|---------|------|-------------|\n| `AUTH_DB` | D1 Database | Better Auth tables (dual mode) |\n| `DB` | D1 Database | Feature tables (dual mode) |\n| `DATABASE` | D1 Database | All tables (single DB mode) |\n| `KV` | KV Namespace | Key-value storage |\n| `R2_BUCKET` | R2 Bucket | File storage (if configured) |\n| `AI` | Workers AI | Embedding generation (if configured) |\n| `VECTORIZE` | Vectorize | Vector similarity search (if configured) |\n| `EMBEDDINGS_QUEUE` | Queue | Async embedding jobs (if configured) |\n| `WEBHOOKS_DB` | D1 Database | Webhook events (if configured) |\n| `WEBHOOKS_QUEUE` | Queue | Webhook delivery (if configured) |\n| `FILES_DB` | D1 Database | File metadata (if R2 configured) |\n| `BROADCASTER` | Service Binding | Realtime broadcast worker (if configured) |\n\n### Worker Variables\n\nSet these in `wrangler.toml` under `[vars]` or in the Cloudflare dashboard:\n\n| Variable | Description | Required |\n|----------|-------------|----------|\n| `BETTER_AUTH_URL` | Public URL of your auth endpoint | Yes |\n| `APP_NAME` | Application name (used in emails) | No |\n\n### Email (AWS SES)\n\nRequired when using the `emailOtp` plugin with AWS SES:\n\n| Variable | Description |\n|----------|-------------|\n| `AWS_ACCESS_KEY_ID` | AWS access key |\n| `AWS_SECRET_ACCESS_KEY` | AWS secret key |\n| `AWS_SES_REGION` | SES region (e.g., `us-east-2`) |\n| `EMAIL_FROM` | Sender email address |\n| `EMAIL_FROM_NAME` | Sender display name |\n| `EMAIL_REPLY_TO` | Reply-to address |\n\n### Drizzle Kit (Migrations)\n\nFor running remote migrations with `drizzle-kit`, set these in `.env`:\n\n| Variable | Description |\n|----------|-------------|\n| `CLOUDFLARE_ACCOUNT_ID` | Your Cloudflare account ID |\n| `CLOUDFLARE_API_TOKEN` | API token with D1 permissions |\n| `CLOUDFLARE_AUTH_DATABASE_ID` | Auth D1 database ID (dual mode) |\n| `CLOUDFLARE_FEATURES_DATABASE_ID` | Features D1 database ID (dual mode) |\n| `CLOUDFLARE_AUDIT_DATABASE_ID` | Security audit D1 database ID (unsafe cross-tenant actions) |\n| `CLOUDFLARE_DATABASE_ID` | Database ID (single DB mode) |\n\n## Bun Variables\n\nSet these in a `.env` file in your project root:\n\n| Variable | Description | Default |\n|----------|-------------|---------|\n| `NODE_ENV` | Runtime environment | `development` |\n| `PORT` | Server port | `3000` |\n| `BETTER_AUTH_SECRET` | Auth encryption secret | — (required) |\n| `BETTER_AUTH_URL` | Public URL of your server | `http://localhost:3000` |\n| `DATABASE_PATH` | Path to SQLite file | `./data/app.db` |\n\n## Turso (LibSQL) Variables\n\nIn addition to the Bun variables above:\n\n| Variable | Description | Default |\n|----------|-------------|---------|\n| `DATABASE_URL` | LibSQL connection URL | `file:./data/app.db` |\n| `DATABASE_AUTH_TOKEN` | Turso auth token (required for remote) | — |\n\n```bash\n# Local development\nDATABASE_URL=file:./data/app.db\n\n# Production (Turso cloud)\nDATABASE_URL=libsql://your-db-slug.turso.io\nDATABASE_AUTH_TOKEN=eyJhbGciOi...\n```\n\n## Social Login Providers\n\nWhen social login is configured in your auth provider:\n\n| Variable | Description |\n|----------|-------------|\n| `GOOGLE_CLIENT_ID` | Google OAuth client ID |\n| `GOOGLE_CLIENT_SECRET` | Google OAuth client secret |\n| `GITHUB_CLIENT_ID` | GitHub OAuth client ID |\n| `GITHUB_CLIENT_SECRET` | GitHub OAuth client secret |\n| `DISCORD_CLIENT_ID` | Discord OAuth client ID |\n| `DISCORD_CLIENT_SECRET` | Discord OAuth client secret |\n\n## See Also\n\n- [Output Structure](/compiler/config/output) — Generated file structure\n- [Providers](/compiler/config/providers) — Provider configuration reference\n- [Cloudflare Template](/compiler/getting-started/template-cloudflare) — Cloudflare setup guide\n- [Bun Template](/compiler/getting-started/template-bun) — Bun setup guide"
147
147
  },
148
148
  "compiler/definitions/access": {
149
149
  "title": "Access - Role & Condition-Based Access Control",
150
- "content": "Define who can perform CRUD operations and under what conditions.\n\n## Basic Usage\n\n```typescript\n// features/rooms/rooms.ts\n\nexport const rooms = sqliteTable('rooms', {\n id: text('id').primaryKey(),\n name: text('name').notNull(),\n organizationId: text('organization_id').notNull(),\n});\n\nexport default defineTable(rooms, {\n firewall: { organization: {} },\n guards: { createable: [\"name\"], updatable: [\"name\"] },\n crud: {\n list: { access: { roles: [\"owner\", \"admin\", \"member\"] } },\n get: { access: { roles: [\"owner\", \"admin\", \"member\"] } },\n create: { access: { roles: [\"owner\", \"admin\"] } },\n update: { access: { roles: [\"owner\", \"admin\"] } },\n delete: { access: { roles: [\"owner\", \"admin\"] } },\n },\n});\n```\n\n## Configuration Options\n\n```typescript\ninterface Access {\n // Required roles (OR logic - user needs at least one)\n roles?: string[];\n\n // Record-level conditions\n record?: {\n [field: string]: FieldCondition;\n };\n\n // Combinators\n or?: Access[];\n and?: Access[];\n}\n\n// Field conditions - value can be string | number | boolean\ntype FieldCondition =\n | { equals: value | '$ctx.userId' | '$ctx.activeOrgId' }\n | { notEquals: value }\n | { in: value[] }\n | { notIn: value[] }\n | { lessThan: number }\n | { greaterThan: number }\n | { lessThanOrEqual: number }\n | { greaterThanOrEqual: number };\n```\n\n## CRUD Configuration\n\n```typescript\ncrud: {\n // LIST - GET /resource\n list: {\n access: { roles: [\"owner\", \"admin\", \"member\"] },\n pageSize: 25, // Default page size\n maxPageSize: 100, // Client can't exceed this\n fields: ['id', 'name', 'status'], // Selective field returns (optional)\n },\n\n // GET - GET /resource/:id\n get: {\n access: { roles: [\"owner\", \"admin\", \"member\"] },\n fields: ['id', 'name', 'status', 'details'], // Optional field selection\n },\n\n // CREATE - POST /resource\n create: {\n access: { roles: [\"owner\", \"admin\"] },\n defaults: { // Default values for new records\n status: 'pending',\n isActive: true,\n },\n },\n\n // UPDATE - PATCH /resource/:id\n update: {\n access: {\n or: [\n { roles: [\"owner\", \"admin\"] },\n { roles: [\"member\"], record: { status: { equals: \"draft\" } } }\n ]\n },\n },\n\n // DELETE - DELETE /resource/:id\n delete: {\n access: { roles: [\"owner\", \"admin\"] },\n mode: \"soft\", // 'soft' (default) or 'hard'\n },\n\n // PUT - PUT /resource/:id (only when generateId: false + guards: false)\n put: {\n access: { roles: [\"admin\", \"sync-service\"] },\n },\n}\n```\n\n## List Filtering (Query Parameters)\n\nThe LIST endpoint automatically supports filtering via query params:\n\n```\nGET /rooms?status=active # Exact match\nGET /rooms?capacity.gt=10 # Greater than\nGET /rooms?capacity.gte=10 # Greater than or equal\nGET /rooms?capacity.lt=50 # Less than\nGET /rooms?capacity.lte=50 # Less than or equal\nGET /rooms?status.ne=deleted # Not equal\nGET /rooms?name.like=Conference # Pattern match (LIKE %value%)\nGET /rooms?status.in=active,pending,review # IN clause\n```\n\n| Operator | Query Param | SQL Equivalent |\n|----------|-------------|----------------|\n| Equals | `?field=value` | `WHERE field = value` |\n| Not equals | `?field.ne=value` | `WHERE field != value` |\n| Greater than | `?field.gt=value` | `WHERE field > value` |\n| Greater or equal | `?field.gte=value` | `WHERE field >= value` |\n| Less than | `?field.lt=value` | `WHERE field < value` |\n| Less or equal | `?field.lte=value` | `WHERE field <= value` |\n| Pattern match | `?field.like=value` | `WHERE field LIKE '%value%'` |\n| In list | `?field.in=a,b,c` | `WHERE field IN ('a','b','c')` |\n\n## Sorting & Pagination\n\n```\nGET /rooms?sort=createdAt&order=desc # Sort by field\nGET /rooms?limit=25&offset=50 # Pagination\n```\n\n- **Default limit**: 50\n- **Max limit**: 100 (or `maxPageSize` if configured)\n- **Default order**: `asc`\n\n## Delete Modes\n\n```typescript\ndelete: {\n access: { roles: [\"owner\", \"admin\"] },\n mode: \"soft\", // Sets deletedAt/deletedBy, record stays in DB\n}\n\ndelete: {\n access: { roles: [\"owner\", \"admin\"] },\n mode: \"hard\", // Permanent deletion from database\n}\n```\n\n## Context Variables\n\nUse `$ctx.` prefix to reference context values in conditions:\n\n```typescript\n// User can only view their own records\naccess: {\n record: { userId: { equals: \"$ctx.userId\" } }\n}\n\n// Nested path support for complex context objects\naccess: {\n record: { ownerId: { equals: \"$ctx.user.id\" } }\n}\n```\n\n### AppContext Reference\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `$ctx.userId` | `string` | Current authenticated user's ID |\n| `$ctx.activeOrgId` | `string` | User's active organization ID |\n| `$ctx.activeTeamId` | `string \\| null` | User's active team ID (if applicable) |\n| `$ctx.roles` | `string[]` | User's roles in current context |\n| `$ctx.isAnonymous` | `boolean` | Whether user is anonymous |\n| `$ctx.user` | `object` | Full user object from auth provider |\n| `$ctx.user.id` | `string` | User ID (nested path example) |\n| `$ctx.user.email` | `string` | User's email address |\n| `$ctx.{property}` | `any` | Any custom context property |\n\n## Function-Based Access\n\nFor complex access logic that can't be expressed declaratively, use a function:\n\n```typescript\ncrud: {\n update: {\n access: async (ctx, record) => {\n // Custom logic - return true to allow, false to deny\n if (ctx.roles.includes('admin')) return true;\n if (record.ownerId === ctx.userId) return true;\n\n // Check custom business logic\n const membership = await checkTeamMembership(ctx.userId, record.teamId);\n return membership.canEdit;\n }\n }\n}\n```\n\nFunction access receives:\n- `ctx`: The full AppContext object\n- `record`: The record being accessed (for get/update/delete operations)"
150
+ "content": "Define who can perform CRUD operations and under what conditions.\n\n## Basic Usage\n\n```typescript\n// features/applications/applications.ts\n\nexport const applications = sqliteTable('applications', {\n id: text('id').primaryKey(),\n candidateId: text('candidate_id').notNull(),\n jobId: text('job_id').notNull(),\n stage: text('stage').notNull(),\n notes: text('notes'),\n organizationId: text('organization_id').notNull(),\n});\n\nexport default defineTable(applications, {\n firewall: { organization: {} },\n guards: { createable: [\"candidateId\", \"jobId\", \"notes\"], updatable: [\"notes\"] },\n crud: {\n list: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\", \"interviewer\"] } },\n get: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\", \"interviewer\"] } },\n create: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\"] } },\n update: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\"] } },\n delete: { access: { roles: [\"owner\", \"hiring-manager\"] } },\n },\n});\n```\n\n## Configuration Options\n\n```typescript\ninterface Access {\n // Required roles (OR logic - user needs at least one)\n roles?: string[];\n\n // Record-level conditions\n record?: {\n [field: string]: FieldCondition;\n };\n\n // Combinators\n or?: Access[];\n and?: Access[];\n}\n\n// Field conditions - value can be string | number | boolean\ntype FieldCondition =\n | { equals: value | '$ctx.userId' | '$ctx.activeOrgId' }\n | { notEquals: value }\n | { in: value[] }\n | { notIn: value[] }\n | { lessThan: number }\n | { greaterThan: number }\n | { lessThanOrEqual: number }\n | { greaterThanOrEqual: number };\n```\n\n## CRUD Configuration\n\n```typescript\ncrud: {\n // LIST - GET /resource\n list: {\n access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\", \"interviewer\"] },\n pageSize: 25, // Default page size\n maxPageSize: 100, // Client can't exceed this\n fields: ['id', 'candidateId', 'jobId', 'stage'], // Selective field returns (optional)\n },\n\n // GET - GET /resource/:id\n get: {\n access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\", \"interviewer\"] },\n fields: ['id', 'candidateId', 'jobId', 'stage', 'notes'], // Optional field selection\n },\n\n // CREATE - POST /resource\n create: {\n access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\"] },\n defaults: { // Default values for new records\n stage: 'applied',\n },\n },\n\n // UPDATE - PATCH /resource/:id\n update: {\n access: {\n or: [\n { roles: [\"hiring-manager\", \"recruiter\"] },\n { roles: [\"interviewer\"], record: { stage: { equals: \"interview\" } } }\n ]\n },\n },\n\n // DELETE - DELETE /resource/:id\n delete: {\n access: { roles: [\"owner\", \"hiring-manager\"] },\n mode: \"soft\", // 'soft' (default) or 'hard'\n },\n\n // PUT - PUT /resource/:id (only when generateId: false + guards: false)\n put: {\n access: { roles: [\"hiring-manager\", \"sync-service\"] },\n },\n}\n```\n\n## List Filtering (Query Parameters)\n\nThe LIST endpoint automatically supports filtering via query params:\n\n```\nGET /jobs?status=open # Exact match\nGET /jobs?salaryMin.gt=50000 # Greater than\nGET /jobs?salaryMin.gte=50000 # Greater than or equal\nGET /jobs?salaryMax.lt=200000 # Less than\nGET /jobs?salaryMax.lte=200000 # Less than or equal\nGET /jobs?status.ne=closed # Not equal\nGET /jobs?title.like=Engineer # Pattern match (LIKE %value%)\nGET /jobs?status.in=open,draft # IN clause\n```\n\n| Operator | Query Param | SQL Equivalent |\n|----------|-------------|----------------|\n| Equals | `?field=value` | `WHERE field = value` |\n| Not equals | `?field.ne=value` | `WHERE field != value` |\n| Greater than | `?field.gt=value` | `WHERE field > value` |\n| Greater or equal | `?field.gte=value` | `WHERE field >= value` |\n| Less than | `?field.lt=value` | `WHERE field < value` |\n| Less or equal | `?field.lte=value` | `WHERE field <= value` |\n| Pattern match | `?field.like=value` | `WHERE field LIKE '%value%'` |\n| In list | `?field.in=a,b,c` | `WHERE field IN ('a','b','c')` |\n\n## Sorting & Pagination\n\n```\nGET /jobs?sort=createdAt&order=desc # Sort by field\nGET /jobs?limit=25&offset=50 # Pagination\n```\n\n- **Default limit**: 50\n- **Max limit**: 100 (or `maxPageSize` if configured)\n- **Default order**: `asc`\n\n## Delete Modes\n\n```typescript\ndelete: {\n access: { roles: [\"owner\", \"hiring-manager\"] },\n mode: \"soft\", // Sets deletedAt/deletedBy, record stays in DB\n}\n\ndelete: {\n access: { roles: [\"owner\", \"hiring-manager\"] },\n mode: \"hard\", // Permanent deletion from database\n}\n```\n\n## Context Variables\n\nUse `$ctx.` prefix to reference context values in conditions:\n\n```typescript\n// User can only view their own records\naccess: {\n record: { userId: { equals: \"$ctx.userId\" } }\n}\n\n// Nested path support for complex context objects\naccess: {\n record: { ownerId: { equals: \"$ctx.user.id\" } }\n}\n```\n\n### AppContext Reference\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `$ctx.userId` | `string` | Current authenticated user's ID |\n| `$ctx.activeOrgId` | `string` | User's active organization ID |\n| `$ctx.activeTeamId` | `string \\| null` | User's active team ID (if applicable) |\n| `$ctx.roles` | `string[]` | User's roles in current context |\n| `$ctx.isAnonymous` | `boolean` | Whether user is anonymous |\n| `$ctx.user` | `object` | Full user object from auth provider |\n| `$ctx.user.id` | `string` | User ID (nested path example) |\n| `$ctx.user.email` | `string` | User's email address |\n| `$ctx.{property}` | `any` | Any custom context property |\n\n## Function-Based Access\n\nFor complex access logic that can't be expressed declaratively, use a function:\n\n```typescript\ncrud: {\n update: {\n access: async (ctx, record) => {\n // Custom logic - return true to allow, false to deny\n if (ctx.roles.includes('admin')) return true;\n if (record.ownerId === ctx.userId) return true;\n\n // Check custom business logic\n const membership = await checkTeamMembership(ctx.userId, record.teamId);\n return membership.canEdit;\n }\n }\n}\n```\n\nFunction access receives:\n- `ctx`: The full AppContext object\n- `record`: The record being accessed (for get/update/delete operations)"
151
151
  },
152
152
  "compiler/definitions/actions": {
153
153
  "title": "Actions",
154
- "content": "Actions are custom API endpoints for business logic beyond CRUD operations. They enable workflows, integrations, and complex operations.\n\n## Overview\n\nQuickback supports two types of actions:\n\n| Aspect | Record-Based | Standalone |\n|--------|--------------|------------|\n| Route | `{METHOD} /:id/{actionName}` | Custom `path` or `/{actionName}` |\n| Record fetching | Automatic | None (`record` is `undefined`) |\n| Firewall applied | Yes | No |\n| Preconditions | Supported via `access.record` | Not applicable |\n| Response types | JSON only | JSON, stream, file |\n| Use case | Approve invoice, archive order | AI chat, bulk import, webhooks |\n\n## Defining Actions\n\nActions are defined in a separate `actions.ts` file that references your table:\n\n```typescript\n// features/invoices/actions.ts\n\nexport default defineActions(invoices, {\n approve: {\n description: \"Approve invoice for payment\",\n input: z.object({ notes: z.string().optional() }),\n access: { roles: [\"admin\", \"finance\"] },\n execute: async ({ db, record, ctx }) => {\n // Business logic\n return record;\n },\n },\n});\n```\n\n### Configuration Options\n\n| Option | Required | Description |\n|--------|----------|-------------|\n| `description` | Yes | Human-readable description of the action |\n| `input` | Yes | Zod schema for request validation |\n| `access` | Yes | Access control (roles, record conditions, or function) |\n| `execute` | Yes* | Inline execution function |\n| `handler` | Yes* | File path for complex logic (alternative to `execute`) |\n| `standalone` | No | Set `true` for non-record actions |\n| `path` | No | Custom route path (standalone only) |\n| `method` | No | HTTP method: GET, POST, PUT, PATCH, DELETE (default: POST) |\n| `responseType` | No | Response format: json, stream, file (default: json) |\n| `sideEffects` | No | Hint for AI tools: 'sync', 'async', or 'fire-and-forget' |\n| `unsafe` | No | When `true`, provides `rawDb` (unscoped) to the executor |\n\n*Either `execute` or `handler` is required, not both.\n\n## Record-Based Actions\n\nRecord-based actions operate on an existing record. The record is automatically loaded and validated before your action executes.\n\n**Route pattern:** `{METHOD} /:id/{actionName}`\n\n```\nPOST /invoices/:id/approve\nGET /orders/:id/status\nDELETE /items/:id/archive\n```\n\n### Runtime Flow\n\n1. **Authentication** - User token is validated\n2. **Record Loading** - The record is fetched by ID\n3. **Firewall Check** - Ensures user can access this record\n4. **Access Check** - Validates roles and preconditions\n5. **Input Validation** - Request body validated against Zod schema\n6. **Execution** - Your action handler runs\n7. **Response** - Result is returned to client\n\n### Example: Invoice Approval\n\n```typescript\n// features/invoices/actions.ts\n\nexport default defineActions(invoices, {\n approve: {\n description: \"Approve invoice for payment\",\n input: z.object({\n notes: z.string().optional(),\n }),\n access: {\n roles: [\"admin\", \"finance\"],\n record: { status: { equals: \"pending\" } }, // Precondition\n },\n execute: async ({ db, ctx, record, input }) => {\n const [updated] = await db\n .update(invoices)\n .set({\n status: \"approved\",\n approvedBy: ctx.userId,\n approvedAt: new Date(),\n })\n .where(eq(invoices.id, record.id))\n .returning();\n\n return updated;\n },\n },\n});\n```\n\n### Request Example\n\n```\nPOST /invoices/inv_123/approve\nContent-Type: application/json\n\n{\n \"notes\": \"Approved for Q1 budget\"\n}\n```\n\n### Response Example\n\n```json\n{\n \"data\": {\n \"id\": \"inv_123\",\n \"status\": \"approved\",\n \"approvedBy\": \"user_456\",\n \"approvedAt\": \"2024-01-15T14:30:00Z\"\n }\n}\n```\n\n## Standalone Actions\n\nStandalone actions are independent endpoints that don't require a record context. Use `standalone: true` and optionally specify a custom `path`.\n\n**Route pattern:** Custom `path` or `/{actionName}`\n\n```\nPOST /chat\nGET /reports/summary\nPOST /webhooks/stripe\n```\n\n### Example: AI Chat with Streaming\n\n```typescript\nexport default defineActions(sessions, {\n chat: {\n description: \"Send a message to AI\",\n standalone: true,\n path: \"/chat\",\n method: \"POST\",\n responseType: \"stream\",\n input: z.object({\n message: z.string().min(1).max(2000),\n }),\n access: {\n roles: [\"member\", \"admin\"],\n },\n handler: \"./handlers/chat\",\n },\n});\n```\n\n### Streaming Response Example\n\nFor actions with `responseType: 'stream'`:\n\n```\nContent-Type: text/event-stream\n\ndata: {\"type\": \"start\"}\ndata: {\"type\": \"chunk\", \"content\": \"Hello\"}\ndata: {\"type\": \"chunk\", \"content\": \"! I'm\"}\ndata: {\"type\": \"chunk\", \"content\": \" here to help.\"}\ndata: {\"type\": \"done\"}\n```\n\n### Example: Report Generation\n\n```typescript\nexport default defineActions(invoices, {\n generateReport: {\n description: \"Generate PDF report\",\n standalone: true,\n path: \"/invoices/report\",\n method: \"GET\",\n responseType: \"file\",\n input: z.object({\n startDate: z.string().datetime(),\n endDate: z.string().datetime(),\n }),\n access: { roles: [\"admin\", \"finance\"] },\n handler: \"./handlers/generate-report\",\n },\n});\n```\n\n## Access Configuration\n\nAccess controls who can execute an action and under what conditions.\n\n### Role-Based Access\n\n```typescript\naccess: {\n roles: [\"admin\", \"manager\"] // OR logic: user needs any of these roles\n}\n```\n\n### Record Conditions\n\nFor record-based actions, you can require the record to be in a specific state:\n\n```typescript\naccess: {\n roles: [\"admin\"],\n record: {\n status: { equals: \"pending\" } // Precondition\n }\n}\n```\n\n**Supported operators:**\n\n| Operator | Description |\n|----------|-------------|\n| `equals` | Field must equal value |\n| `notEquals` | Field must not equal value |\n| `in` | Field must be one of the values |\n| `notIn` | Field must not be one of the values |\n\n### Context Substitution\n\nUse `$ctx` to reference the current user's context:\n\n```typescript\naccess: {\n record: {\n ownerId: { equals: \"$ctx.userId\" },\n orgId: { equals: \"$ctx.orgId\" }\n }\n}\n```\n\n### OR/AND Combinations\n\n```typescript\naccess: {\n or: [\n { roles: [\"admin\"] },\n {\n roles: [\"member\"],\n record: { ownerId: { equals: \"$ctx.userId\" } }\n }\n ]\n}\n```\n\n### Function Access\n\nFor complex logic, use an access function:\n\n```typescript\naccess: async (ctx, record) => {\n return ctx.roles.includes('admin') || record.ownerId === ctx.userId;\n}\n```\n\n## Scoped Database\n\nAll actions receive a **scoped `db`** instance that automatically enforces security:\n\n| Operation | Org Scoping | Owner Scoping | Soft Delete Filter | Auto-inject on INSERT |\n|-----------|-------------|---------------|--------------------|-----------------------|\n| `SELECT` | `WHERE organizationId = ?` | `WHERE ownerId = ?` | `WHERE deletedAt IS NULL` | n/a |\n| `INSERT` | n/a | n/a | n/a | `organizationId`, `ownerId` from ctx |\n| `UPDATE` | `WHERE organizationId = ?` | `WHERE ownerId = ?` | `WHERE deletedAt IS NULL` | n/a |\n| `DELETE` | `WHERE organizationId = ?` | `WHERE ownerId = ?` | `WHERE deletedAt IS NULL` | n/a |\n\nScoping is duck-typed at runtime — tables with an `organizationId` column get org scoping, tables with `ownerId` get owner scoping, tables with `deletedAt` get soft delete filtering.\n\n```typescript\nexecute: async ({ db, ctx, input }) => {\n // This query automatically includes WHERE organizationId = ? AND ownerId = ? AND deletedAt IS NULL\n const items = await db.select().from(claims).where(eq(claims.status, 'active'));\n\n // Inserts automatically include organizationId and ownerId\n await db.insert(claims).values({ title: input.title });\n\n return items;\n}\n```\n\n**Not enforced** in scoped DB (by design):\n- **Guards** — actions ARE the authorized way to modify guarded fields\n- **Masking** — actions are backend code that may need raw data\n- **Access** — already checked before action execution\n\n### Unsafe Mode\n\nActions that need to bypass security (e.g., cross-org admin queries) can declare `unsafe: true`:\n\n```typescript\nexport default defineActions(invoices, {\n adminReport: {\n description: \"Generate cross-org report\",\n unsafe: true,\n input: z.object({ startDate: z.string() }),\n access: { roles: [\"admin\"] },\n execute: async ({ db, rawDb, ctx, input }) => {\n // db is still scoped (safety net)\n // rawDb bypasses all security — only available with unsafe: true\n const allOrgs = await rawDb.select().from(invoices);\n return allOrgs;\n },\n },\n});\n```\n\nWithout `unsafe: true`, `rawDb` is `undefined` in the executor params.\n\n## Handler Files\n\nFor complex actions, separate the logic into handler files.\n\n### When to Use Handler Files\n\n- Complex business logic spanning multiple operations\n- External API integrations\n- File generation or processing\n- Logic reused across multiple actions\n\n### Handler Structure\n\n```typescript\n// handlers/generate-report.ts\n\nexport const execute: ActionExecutor = async ({ db, ctx, input, services }) => {\n const invoices = await db\n .select()\n .from(invoicesTable)\n .where(between(invoicesTable.createdAt, input.startDate, input.endDate));\n\n const pdf = await services.pdf.generate(invoices);\n\n return {\n file: pdf,\n filename: `invoices-${input.startDate}-${input.endDate}.pdf`,\n contentType: 'application/pdf',\n };\n};\n```\n\n### Importing Tables\n\nHandler files can import tables from their own feature or other features. The compiler generates alias files for each table, so you import by the table's file name:\n\n```typescript\n// handlers/post-entry.ts\n\n// Same feature — import from parent directory using the table's file name\n\n// Cross-feature — go up to the features directory, then into the other feature\n\nexport const execute: ActionExecutor = async ({ db, ctx, input, c }) => {\n const [account] = await db\n .select()\n .from(ledgerAccounts)\n .where(eq(ledgerAccounts.id, input.accountId))\n .limit(1);\n\n if (!account) {\n return c.json({ error: 'Account not found', code: 'NOT_FOUND' }, 404);\n }\n\n // ... continue with business logic\n};\n```\n\n**Path pattern from `features/{name}/handlers/`:**\n- Same feature table: `../{table-file-name}` (e.g., `../invoices`)\n- Other feature table: `../../{other-feature}/{table-file-name}` (e.g., `../../accounts/ledger-accounts`)\n- Generated lib files: `../../../lib/{module}` (e.g., `../../../lib/realtime`, `../../../lib/webhooks`)\n\n### Executor Parameters\n\n```typescript\ninterface ActionExecutorParams {\n db: DrizzleDB; // Scoped database (auto-applies org/owner/soft-delete filters)\n rawDb?: DrizzleDB; // Raw database (only available when unsafe: true)\n ctx: AppContext; // User context (userId, roles, orgId)\n record?: TRecord; // The record (record-based only, undefined for standalone)\n input: TInput; // Validated input from Zod schema\n services: TServices; // Configured integrations (billing, notifications, etc.)\n c: HonoContext; // Raw Hono context for advanced use\n auditFields: object; // { createdAt, modifiedAt } timestamps\n}\n```\n\n## HTTP API Reference\n\n### Request Format\n\n| Method | Input Source | Use Case |\n|--------|--------------|----------|\n| `GET` | Query parameters | Read-only operations, fetching data |\n| `POST` | JSON body | Default, state-changing operations |\n| `PUT` | JSON body | Full replacement operations |\n| `PATCH` | JSON body | Partial updates |\n| `DELETE` | JSON body | Deletion with optional payload |\n\n```typescript\n// GET action - input comes from query params\ngetStatus: {\n method: \"GET\",\n input: z.object({ format: z.string().optional() }),\n // Called as: GET /invoices/:id/getStatus?format=detailed\n}\n\n// POST action (default) - input comes from JSON body\napprove: {\n // method: \"POST\" is implied\n input: z.object({ notes: z.string().optional() }),\n // Called as: POST /invoices/:id/approve with JSON body\n}\n```\n\n### Response Formats\n\n| Type | Content-Type | Use Case |\n|------|--------------|----------|\n| `json` | `application/json` | Standard API responses (default) |\n| `stream` | `text/event-stream` | Real-time streaming (AI chat, live updates) |\n| `file` | Varies | File downloads (reports, exports) |\n\n### Error Codes\n\n| Status | Description |\n|--------|-------------|\n| `400` | Invalid input / validation error |\n| `401` | Not authenticated |\n| `403` | Access check failed (role or precondition) |\n| `404` | Record not found (record-based actions) |\n| `500` | Handler execution error |\n\n### Validation Error Response\n\n```json\n{\n \"error\": \"Invalid request data\",\n \"layer\": \"validation\",\n \"code\": \"VALIDATION_FAILED\",\n \"details\": {\n \"fields\": {\n \"amount\": \"Expected positive number\"\n }\n },\n \"hint\": \"Check the input schema for this action\"\n}\n```\n\n### Error Handling\n\n**Option 1: Return a JSON error response** (recommended for most cases)\n\nSince action handlers receive the Hono context (`c`), you can return error responses directly:\n\n```typescript\nexecute: async ({ ctx, record, input, c }) => {\n if (record.balance < input.amount) {\n return c.json({\n error: 'Not enough balance',\n code: 'INSUFFICIENT_FUNDS',\n details: { required: input.amount, available: record.balance },\n }, 400);\n }\n // ... continue\n}\n```\n\n**Option 2: Throw an ActionError**\n\n```typescript\n\nexecute: async ({ ctx, record, input }) => {\n if (record.balance < input.amount) {\n throw new ActionError('Not enough balance', 'INSUFFICIENT_FUNDS', 400, {\n required: input.amount,\n available: record.balance,\n });\n }\n // ... continue\n}\n```\n\nThe `ActionError` constructor signature is `(message, code, statusCode, details?)`.\n\n## Protected Fields\n\nActions can modify fields that are protected from regular CRUD operations:\n\n```typescript\n// In resource.ts\nguards: {\n protected: {\n status: [\"approve\", \"reject\"], // Only these actions can modify status\n amount: [\"reviseAmount\"],\n }\n}\n```\n\nThis allows the `approve` action to set `status = \"approved\"` even though the field is protected from regular PATCH requests.\n\n## Examples\n\n### Invoice Approval (Record-Based)\n\n```typescript\nexport default defineActions(invoices, {\n approve: {\n description: \"Approve invoice for payment\",\n input: z.object({ notes: z.string().optional() }),\n access: {\n roles: [\"admin\", \"finance\"],\n record: { status: { equals: \"pending\" } },\n },\n execute: async ({ db, ctx, record, input }) => {\n const [updated] = await db\n .update(invoices)\n .set({\n status: \"approved\",\n approvedBy: ctx.userId,\n notes: input.notes,\n })\n .where(eq(invoices.id, record.id))\n .returning();\n return updated;\n },\n },\n});\n```\n\n### AI Chat (Standalone with Streaming)\n\n```typescript\nexport default defineActions(sessions, {\n chat: {\n description: \"Send a message to AI assistant\",\n standalone: true,\n path: \"/chat\",\n responseType: \"stream\",\n input: z.object({\n message: z.string().min(1).max(2000),\n }),\n access: { roles: [\"member\", \"admin\"] },\n handler: \"./handlers/chat\",\n },\n});\n```\n\n### Bulk Import (Standalone, No Record)\n\n```typescript\nexport default defineActions(contacts, {\n bulkImport: {\n description: \"Import contacts from CSV\",\n standalone: true,\n path: \"/contacts/import\",\n input: z.object({\n data: z.array(z.object({\n email: z.string().email(),\n name: z.string(),\n })),\n }),\n access: { roles: [\"admin\"] },\n execute: async ({ db, input }) => {\n const inserted = await db\n .insert(contacts)\n .values(input.data)\n .returning();\n return { imported: inserted.length };\n },\n },\n});\n```"
154
+ "content": "Actions are custom API endpoints for business logic beyond CRUD operations. They enable workflows, integrations, and complex operations.\n\n## Overview\n\nQuickback supports two types of actions:\n\n| Aspect | Record-Based | Standalone |\n|--------|--------------|------------|\n| Route | `{METHOD} /:id/{actionName}` | Custom `path` or `/{actionName}` |\n| Record fetching | Automatic | None (`record` is `undefined`) |\n| Firewall applied | Yes | No |\n| Preconditions | Supported via `access.record` | Not applicable |\n| Response types | JSON only | JSON, stream, file |\n| Use case | Advance application, reject candidate | AI chat, bulk import from job board, webhooks |\n\n## Defining Actions\n\nActions are defined in a separate `actions.ts` file that references your table:\n\n```typescript\n// features/applications/actions.ts\n\nexport default defineActions(applications, {\n 'advance-stage': {\n description: \"Move application to the next pipeline stage\",\n input: z.object({ stage: z.enum([\"screening\", \"interview\", \"offer\", \"hired\"]), notes: z.string().optional() }),\n access: { roles: [\"owner\", \"hiring-manager\"] },\n execute: async ({ db, record, ctx }) => {\n // Business logic\n return record;\n },\n },\n});\n```\n\n### Configuration Options\n\n| Option | Required | Description |\n|--------|----------|-------------|\n| `description` | Yes | Human-readable description of the action |\n| `input` | Yes | Zod schema for request validation |\n| `access` | Yes | Access control (roles, record conditions, or function) |\n| `execute` | Yes* | Inline execution function |\n| `handler` | Yes* | File path for complex logic (alternative to `execute`) |\n| `standalone` | No | Set `true` for non-record actions |\n| `path` | No | Custom route path (standalone only) |\n| `method` | No | HTTP method: GET, POST, PUT, PATCH, DELETE (default: POST) |\n| `responseType` | No | Response format: json, stream, file (default: json) |\n| `sideEffects` | No | Hint for AI tools: 'sync', 'async', or 'fire-and-forget' |\n| `allowRawSql` | No | Explicit compile-time opt-in for raw SQL in execute/handler code |\n| `unsafe` | No | Unsafe raw DB mode. Use `true` (legacy) or object config (`reason`, `adminOnly`, `crossTenant`, `targetScope`) |\n\n*Either `execute` or `handler` is required, not both.\n\n## Record-Based Actions\n\nRecord-based actions operate on an existing record. The record is automatically loaded and validated before your action executes.\n\n**Route pattern:** `{METHOD} /:id/{actionName}`\n\n```\nPOST /applications/:id/advance-stage\nPOST /applications/:id/reject\nPOST /applications/:id/schedule-interview\n```\n\n### Runtime Flow\n\n1. **Authentication** - User token is validated\n2. **Record Loading** - The record is fetched by ID\n3. **Firewall Check** - Ensures user can access this record\n4. **Access Check** - Validates roles and preconditions\n5. **Input Validation** - Request body validated against Zod schema\n6. **Execution** - Your action handler runs\n7. **Response** - Result is returned to client\n\n### Example: Advance Application Stage\n\n```typescript\n// features/applications/actions.ts\n\nexport default defineActions(applications, {\n 'advance-stage': {\n description: \"Move application to the next pipeline stage\",\n input: z.object({\n stage: z.enum([\"screening\", \"interview\", \"offer\", \"hired\"]),\n notes: z.string().optional(),\n }),\n access: {\n roles: [\"owner\", \"hiring-manager\"],\n record: { stage: { notEquals: \"rejected\" } }, // Precondition\n },\n execute: async ({ db, ctx, record, input }) => {\n const [updated] = await db\n .update(applications)\n .set({\n stage: input.stage,\n notes: input.notes ?? record.notes,\n })\n .where(eq(applications.id, record.id))\n .returning();\n\n return updated;\n },\n },\n});\n```\n\n### Request Example\n\n```\nPOST /applications/app_123/advance-stage\nContent-Type: application/json\n\n{\n \"stage\": \"interview\",\n \"notes\": \"Strong technical background, moving to interview\"\n}\n```\n\n### Response Example\n\n```json\n{\n \"data\": {\n \"id\": \"app_123\",\n \"stage\": \"interview\",\n \"notes\": \"Strong technical background, moving to interview\"\n }\n}\n```\n\n## Standalone Actions\n\nStandalone actions are independent endpoints that don't require a record context. Use `standalone: true` and optionally specify a custom `path`.\n\n**Route pattern:** Custom `path` or `/{actionName}`\n\n```\nPOST /chat\nGET /reports/summary\nPOST /webhooks/stripe\n```\n\n### Example: AI Chat with Streaming\n\n```typescript\nexport default defineActions(sessions, {\n chat: {\n description: \"Send a message to AI\",\n standalone: true,\n path: \"/chat\",\n method: \"POST\",\n responseType: \"stream\",\n input: z.object({\n message: z.string().min(1).max(2000),\n }),\n access: {\n roles: [\"recruiter\", \"hiring-manager\"],\n },\n handler: \"./handlers/chat\",\n },\n});\n```\n\n### Streaming Response Example\n\nFor actions with `responseType: 'stream'`:\n\n```\nContent-Type: text/event-stream\n\ndata: {\"type\": \"start\"}\ndata: {\"type\": \"chunk\", \"content\": \"Hello\"}\ndata: {\"type\": \"chunk\", \"content\": \"! I'm\"}\ndata: {\"type\": \"chunk\", \"content\": \" here to help.\"}\ndata: {\"type\": \"done\"}\n```\n\n### Example: Report Generation\n\n```typescript\nexport default defineActions(applications, {\n generateReport: {\n description: \"Generate hiring pipeline report\",\n standalone: true,\n path: \"/applications/report\",\n method: \"GET\",\n responseType: \"file\",\n input: z.object({\n startDate: z.string().datetime(),\n endDate: z.string().datetime(),\n }),\n access: { roles: [\"owner\", \"hiring-manager\"] },\n handler: \"./handlers/generate-report\",\n },\n});\n```\n\n## Access Configuration\n\nAccess controls who can execute an action and under what conditions.\n\n### Role-Based Access\n\n```typescript\naccess: {\n roles: [\"hiring-manager\", \"recruiter\"] // OR logic: user needs any of these roles\n}\n```\n\n### Record Conditions\n\nFor record-based actions, you can require the record to be in a specific state:\n\n```typescript\naccess: {\n roles: [\"hiring-manager\"],\n record: {\n stage: { equals: \"screening\" } // Precondition\n }\n}\n```\n\n**Supported operators:**\n\n| Operator | Description |\n|----------|-------------|\n| `equals` | Field must equal value |\n| `notEquals` | Field must not equal value |\n| `in` | Field must be one of the values |\n| `notIn` | Field must not be one of the values |\n\n### Context Substitution\n\nUse `$ctx` to reference the current user's context:\n\n```typescript\naccess: {\n record: {\n ownerId: { equals: \"$ctx.userId\" },\n orgId: { equals: \"$ctx.orgId\" }\n }\n}\n```\n\n### OR/AND Combinations\n\n```typescript\naccess: {\n or: [\n { roles: [\"hiring-manager\"] },\n {\n roles: [\"recruiter\"],\n record: { ownerId: { equals: \"$ctx.userId\" } }\n }\n ]\n}\n```\n\n### Function Access\n\nFor complex logic, use an access function:\n\n```typescript\naccess: async (ctx, record) => {\n return ctx.roles.includes('admin') || record.ownerId === ctx.userId;\n}\n```\n\n## Scoped Database\n\nAll actions receive a **scoped `db`** instance that automatically enforces security:\n\n| Operation | Org Scoping | Owner Scoping | Soft Delete Filter | Auto-inject on INSERT |\n|-----------|-------------|---------------|--------------------|-----------------------|\n| `SELECT` | `WHERE organizationId = ?` | `WHERE ownerId = ?` | `WHERE deletedAt IS NULL` | n/a |\n| `INSERT` | n/a | n/a | n/a | `organizationId`, `ownerId` from ctx |\n| `UPDATE` | `WHERE organizationId = ?` | `WHERE ownerId = ?` | `WHERE deletedAt IS NULL` | n/a |\n| `DELETE` | `WHERE organizationId = ?` | `WHERE ownerId = ?` | `WHERE deletedAt IS NULL` | n/a |\n\nScoping is duck-typed at runtime — tables with an `organizationId` column get org scoping, tables with `ownerId` get owner scoping, tables with `deletedAt` get soft delete filtering.\n\n```typescript\nexecute: async ({ db, ctx, input }) => {\n // This query automatically includes WHERE organizationId = ? AND deletedAt IS NULL\n const items = await db.select().from(applications).where(eq(applications.stage, 'interview'));\n\n // Inserts automatically include organizationId\n await db.insert(applications).values({ candidateId: input.candidateId, jobId: input.jobId, stage: 'applied' });\n\n return items;\n}\n```\n\n**Not enforced** in scoped DB (by design):\n- **Guards** — actions ARE the authorized way to modify guarded fields\n- **Masking** — actions are backend code that may need raw data\n- **Access** — already checked before action execution\n\n### Unsafe Mode\n\nActions that need to bypass scoped DB filters (for example, platform-level support operations) can enable unsafe mode.\n\nUse object form for explicit policy and mandatory audit metadata:\n\n```typescript\nexport default defineActions(applications, {\n adminReport: {\n description: \"Generate cross-org hiring report\",\n unsafe: {\n reason: \"Support investigation for enterprise customer\",\n adminOnly: true, // default true\n crossTenant: true, // default true\n targetScope: \"all\", // \"all\" | \"organization\"\n },\n input: z.object({ startDate: z.string() }),\n access: { roles: [\"owner\"] },\n execute: async ({ db, rawDb, ctx, input }) => {\n // db is still scoped (safety net)\n // rawDb bypasses scoped filters\n const allOrgs = await rawDb.select().from(applications);\n return allOrgs;\n },\n },\n});\n```\n\nUnsafe cross-tenant actions are generated with:\n\n- Better Auth authentication required (no unauthenticated path)\n- platform admin gate (`ctx.userRole === \"admin\"`)\n- mandatory audit logging on deny/success/error\n\nWithout unsafe mode, `rawDb` is `undefined` in the executor params.\n\nLegacy `unsafe: true` is still supported, but object mode is recommended so audit logs include an explicit reason.\n\n### Raw SQL Policy\n\nBy default, the compiler rejects raw SQL in action code and handler files. Use Drizzle query-builder syntax whenever possible.\n\nIf a specific action must use raw SQL, opt in explicitly:\n\n```typescript\nreconcileLedger: {\n description: \"Run custom reconciliation query\",\n allowRawSql: true,\n input: z.object({}),\n access: { roles: [\"owner\"] },\n execute: async ({ db }) => {\n // Allowed because allowRawSql: true\n return db.execute(sql`select 1`);\n },\n}\n```\n\nWithout `allowRawSql: true`, compilation fails with a loud error pointing to the action and snippet.\n\n## Handler Files\n\nFor complex actions, separate the logic into handler files.\n\n### When to Use Handler Files\n\n- Complex business logic spanning multiple operations\n- External API integrations\n- File generation or processing\n- Logic reused across multiple actions\n\n### Handler Structure\n\n```typescript\n// handlers/generate-report.ts\n\nexport const execute: ActionExecutor = async ({ db, ctx, input, services }) => {\n const apps = await db\n .select()\n .from(applicationsTable)\n .where(between(applicationsTable.createdAt, input.startDate, input.endDate));\n\n const pdf = await services.pdf.generate(apps);\n\n return {\n file: pdf,\n filename: `hiring-report-${input.startDate}-${input.endDate}.pdf`,\n contentType: 'application/pdf',\n };\n};\n```\n\n### Importing Tables\n\nHandler files can import tables from their own feature or other features. The compiler generates alias files for each table, so you import by the table's file name:\n\n```typescript\n// handlers/advance-stage.ts\n\n// Same feature — import from parent directory using the table's file name\n\n// Cross-feature — go up to the features directory, then into the other feature\n\nexport const execute: ActionExecutor = async ({ db, ctx, input, c }) => {\n const [job] = await db\n .select()\n .from(jobs)\n .where(eq(jobs.id, input.jobId))\n .limit(1);\n\n if (!job) {\n return c.json({ error: 'Job not found', code: 'NOT_FOUND' }, 404);\n }\n\n // ... continue with business logic\n};\n```\n\n**Path pattern from `features/{name}/handlers/`:**\n- Same feature table: `../{table-file-name}` (e.g., `../applications`)\n- Other feature table: `../../{other-feature}/{table-file-name}` (e.g., `../../candidates/candidates`)\n- Generated lib files: `../../../lib/{module}` (e.g., `../../../lib/realtime`, `../../../lib/webhooks`)\n\n### Executor Parameters\n\n```typescript\ninterface ActionExecutorParams {\n db: DrizzleDB; // Scoped database (auto-applies org/owner/soft-delete filters)\n rawDb?: DrizzleDB; // Raw database (only available when unsafe mode is enabled)\n ctx: AppContext; // User context (userId, roles, orgId)\n record?: TRecord; // The record (record-based only, undefined for standalone)\n input: TInput; // Validated input from Zod schema\n services: TServices; // Configured integrations (billing, notifications, etc.)\n c: HonoContext; // Raw Hono context for advanced use\n auditFields: object; // { createdAt, modifiedAt } timestamps\n}\n```\n\n## HTTP API Reference\n\n### Request Format\n\n| Method | Input Source | Use Case |\n|--------|--------------|----------|\n| `GET` | Query parameters | Read-only operations, fetching data |\n| `POST` | JSON body | Default, state-changing operations |\n| `PUT` | JSON body | Full replacement operations |\n| `PATCH` | JSON body | Partial updates |\n| `DELETE` | JSON body | Deletion with optional payload |\n\n```typescript\n// GET action - input comes from query params\ngetStageHistory: {\n method: \"GET\",\n input: z.object({ format: z.string().optional() }),\n // Called as: GET /applications/:id/getStageHistory?format=detailed\n}\n\n// POST action (default) - input comes from JSON body\n'advance-stage': {\n // method: \"POST\" is implied\n input: z.object({ stage: z.enum([\"screening\", \"interview\", \"offer\", \"hired\"]) }),\n // Called as: POST /applications/:id/advance-stage with JSON body\n}\n```\n\n### Response Formats\n\n| Type | Content-Type | Use Case |\n|------|--------------|----------|\n| `json` | `application/json` | Standard API responses (default) |\n| `stream` | `text/event-stream` | Real-time streaming (AI chat, live updates) |\n| `file` | Varies | File downloads (reports, exports) |\n\n### Error Codes\n\n| Status | Description |\n|--------|-------------|\n| `400` | Invalid input / validation error |\n| `401` | Not authenticated |\n| `403` | Access check failed (role or precondition) |\n| `404` | Record not found (record-based actions) |\n| `500` | Handler execution error |\n\n### Validation Error Response\n\n```json\n{\n \"error\": \"Invalid request data\",\n \"layer\": \"validation\",\n \"code\": \"VALIDATION_FAILED\",\n \"details\": {\n \"fields\": {\n \"amount\": \"Expected positive number\"\n }\n },\n \"hint\": \"Check the input schema for this action\"\n}\n```\n\n### Error Handling\n\n**Option 1: Return a JSON error response** (recommended for most cases)\n\nSince action handlers receive the Hono context (`c`), you can return error responses directly:\n\n```typescript\nexecute: async ({ ctx, record, input, c }) => {\n if (record.stage === 'hired') {\n return c.json({\n error: 'Cannot modify a hired application',\n code: 'ALREADY_HIRED',\n details: { currentStage: record.stage },\n }, 400);\n }\n // ... continue\n}\n```\n\n**Option 2: Throw an ActionError**\n\n```typescript\n\nexecute: async ({ ctx, record, input }) => {\n if (record.stage === 'hired') {\n throw new ActionError('Cannot modify a hired application', 'ALREADY_HIRED', 400, {\n currentStage: record.stage,\n });\n }\n // ... continue\n}\n```\n\nThe `ActionError` constructor signature is `(message, code, statusCode, details?)`.\n\n## Protected Fields\n\nActions can modify fields that are protected from regular CRUD operations:\n\n```typescript\n// In resource.ts\nguards: {\n protected: {\n stage: [\"advance-stage\", \"reject\"], // Only these actions can modify stage\n }\n}\n```\n\nThis allows the `advance-stage` action to set `stage = \"interview\"` even though the field is protected from regular PATCH requests.\n\n## Examples\n\n### Application Stage Advance (Record-Based)\n\n```typescript\nexport default defineActions(applications, {\n 'advance-stage': {\n description: \"Move application to the next pipeline stage\",\n input: z.object({\n stage: z.enum([\"screening\", \"interview\", \"offer\", \"hired\"]),\n notes: z.string().optional(),\n }),\n access: {\n roles: [\"owner\", \"hiring-manager\"],\n record: { stage: { notEquals: \"rejected\" } },\n },\n execute: async ({ db, ctx, record, input }) => {\n const [updated] = await db\n .update(applications)\n .set({\n stage: input.stage,\n notes: input.notes ?? record.notes,\n })\n .where(eq(applications.id, record.id))\n .returning();\n return updated;\n },\n },\n});\n```\n\n### AI Chat (Standalone with Streaming)\n\n```typescript\nexport default defineActions(sessions, {\n chat: {\n description: \"Send a message to AI assistant\",\n standalone: true,\n path: \"/chat\",\n responseType: \"stream\",\n input: z.object({\n message: z.string().min(1).max(2000),\n }),\n access: { roles: [\"recruiter\", \"hiring-manager\"] },\n handler: \"./handlers/chat\",\n },\n});\n```\n\n### Bulk Import (Standalone, No Record)\n\n```typescript\nexport default defineActions(candidates, {\n bulkImport: {\n description: \"Import candidates from job board CSV\",\n standalone: true,\n path: \"/candidates/import\",\n input: z.object({\n data: z.array(z.object({\n email: z.string().email(),\n name: z.string(),\n })),\n }),\n access: { roles: [\"hiring-manager\", \"recruiter\"] },\n execute: async ({ db, input }) => {\n const inserted = await db\n .insert(candidates)\n .values(input.data)\n .returning();\n return { imported: inserted.length };\n },\n },\n});\n```"
155
+ },
156
+ "compiler/definitions/concepts": {
157
+ "title": "Glossary",
158
+ "content": "Quick reference for terms used across the Quickback documentation.\n\n## Core Concepts\n\n| Term | Definition |\n|------|-----------|\n| **Feature** | A directory in `quickback/features/` containing related tables and actions. Example: a `candidates` feature with `candidates.ts`, `candidate-notes.ts`, and `actions.ts`. |\n| **Resource** | A table with a `defineTable()` default export. Each resource gets its own CRUD API endpoints (`GET`, `POST`, `PATCH`, `DELETE`, plus batch operations). Example: `candidates.ts` with `defineTable()` generates `/api/v1/candidates`. |\n| **Internal Table** | A Drizzle table exported WITHOUT `defineTable()`. Used as supporting data (junction tables, lookup tables) — no API routes generated. |\n| **Definition** | A TypeScript file in `quickback/features/` that defines a table schema and/or security configuration. |\n| **Compilation** | The process of transforming your definitions into production-ready code: routes, middleware, types, and migrations. |\n\n## Security Layers\n\n| Term | Definition |\n|------|-----------|\n| **Firewall** | Row-level data isolation. Automatically adds WHERE clauses (e.g., `WHERE organizationId = ?`) to every query so users only see data they should. |\n| **Access** | CRUD operation permissions. Controls which roles can perform list, get, create, update, and delete operations. |\n| **Guards** | Field-level write protection. Controls which fields can be set on create (`createable`), modified on update (`updatable`), changed only via actions (`protected`), or never changed after creation (`immutable`). |\n| **Masking** | Field-level read protection. Redacts sensitive values (SSN, email, phone) in API responses based on the user's role. |\n| **Views** | Column-level projections. Named subsets of fields (e.g., \"summary\", \"full\") with their own access control. Accessed via `GET /api/v1/{resource}/views/{name}`. |\n\n## Actions\n\n| Term | Definition |\n|------|-----------|\n| **Action** | Custom business logic endpoint beyond CRUD. Defined with `defineActions()` in an `actions.ts` file. |\n| **Record Action** | An action that operates on a specific record (`POST /api/v1/{resource}/:id/{action}`). Receives the record in the handler. Example: `POST /api/v1/applications/:id/advance-stage`. |\n| **Standalone Action** | An action not tied to a specific record (`POST /api/v1/{resource}/{action}`). Used for hiring reports, AI resume screening, bulk candidate imports. |\n| **Scoped DB** | The security-filtered database handle passed to action handlers. Automatically applies firewall, soft-delete, and org isolation. |\n| **Unsafe Mode** | When an action enables `unsafe` (prefer object form), it receives `rawDb` for explicit admin/cross-tenant operations. Cross-tenant mode requires admin role + audit trail. |\n\n## Configuration\n\n| Term | Definition |\n|------|-----------|\n| **`defineTable()`** | The function that combines a Drizzle schema with security configuration. Imported from `@quickback/compiler`. |\n| **`defineActions()`** | The function that defines custom actions for a table. Imported from `@quickback/compiler`. |\n| **`defineConfig()`** | The function that configures your Quickback project (runtime, database, auth providers). Lives in `quickback/quickback.config.ts`. |\n| **Audit Fields** | Auto-injected columns: `createdAt`, `createdBy`, `modifiedAt`, `modifiedBy`, `deletedAt`, `deletedBy`. You don't define these — the compiler adds them. |\n| **Soft Delete** | Default delete behavior. Sets `deletedAt` instead of removing the row. Soft-deleted records are filtered from queries automatically. |\n| **Hard Delete** | Permanent row removal. Configured per-resource with `delete: { mode: \"hard\" }`. |\n\n## Infrastructure\n\n| Term | Definition |\n|------|-----------|\n| **Cloud Compiler** | The remote compilation service at `compiler.quickback.dev`. The CLI sends your definitions and receives generated code back. |\n| **Stack** | The runtime infrastructure (Cloudflare Workers, D1, KV, R2, Better Auth) where your compiled API runs. |\n| **Template** | A pre-configured project starter (`cloudflare`, `bun`, `turso`) created by `quickback create`. |"
155
159
  },
156
160
  "compiler/definitions/firewall": {
157
161
  "title": "Firewall - Data Isolation",
158
- "content": "The firewall generates WHERE clauses automatically to isolate data by user, organization, or team.\n\n## Basic Usage\n\n```typescript\n// features/rooms/rooms.ts\n\nexport const rooms = sqliteTable('rooms', {\n id: text('id').primaryKey(),\n name: text('name').notNull(),\n organizationId: text('organization_id').notNull(),\n});\n\nexport default defineTable(rooms, {\n firewall: { organization: {} }, // Isolate by organization\n // ... guards, crud\n});\n```\n\n## Configuration Options\n\n```typescript\nfirewall: {\n // User-level ownership (personal data)\n owner?: {\n column?: string; // Default: 'ownerId' or 'owner_id'\n source?: string; // Default: 'ctx.userId'\n mode?: 'required' | 'optional'; // Default: 'required'\n };\n\n // Organization-level ownership\n organization?: {\n column?: string; // Default: 'organizationId' or 'organization_id'\n source?: string; // Default: 'ctx.activeOrgId'\n };\n\n // Team-level ownership\n team?: {\n column?: string; // Default: 'teamId' or 'team_id'\n source?: string; // Default: 'ctx.activeTeamId'\n };\n\n // Soft delete filtering\n softDelete?: {\n column?: string; // Default: 'deletedAt'\n };\n\n // Error reporting mode for firewall violations\n // 'reveal' (default): 403 with structured error message\n // 'hide': Generic 404 (prevents record enumeration)\n errorMode?: 'reveal' | 'hide';\n\n // Opt-out for public tables (cannot combine with ownership)\n exception?: boolean;\n}\n```\n\n## Context Sources\n\nThe firewall pulls ownership values from the request context:\n\n| Scope | Default Source | Description |\n|-------|----------------|-------------|\n| `owner` | `ctx.userId` | Current authenticated user's ID |\n| `organization` | `ctx.activeOrgId` | User's active organization ID |\n| `team` | `ctx.activeTeamId` | User's active team ID |\n\nThese context values are populated by the auth middleware from the user's session.\n```\n\n## Auto-Detection\n\nQuickback automatically detects firewall scope based on your column names:\n\n| Column Name | Detected Scope |\n|-------------|----------------|\n| `organizationId` or `organization_id` | `organization` |\n| `ownerId` or `owner_id` | `owner` |\n| `teamId` or `team_id` | `team` |\n| `deletedAt` or `deleted_at` | `softDelete` |\n\nThis means you often don't need to specify column names:\n\n```typescript\n// Quickback detects organizationId column automatically\nfirewall: { organization: {} }\n\n// Quickback detects ownerId column automatically\nfirewall: { owner: {} }\n```\n\n## Common Patterns\n\n```typescript\n// Public/system table - no filtering\nfirewall: { exception: true }\n\n// Organization-scoped data (auto-detected from organizationId column)\nfirewall: { organization: {} }\n\n// Personal user data (auto-detected from ownerId column)\nfirewall: { owner: {} }\n\n// Org data with optional owner filtering\nfirewall: {\n organization: {},\n owner: { mode: 'optional' }\n}\n\n// With soft delete (auto-detected from deletedAt column)\nfirewall: {\n organization: {},\n softDelete: {}\n}\n```\n\n## Error Responses\n\nWhen a record isn't found behind the firewall (wrong org, soft-deleted, or genuinely doesn't exist), the error response depends on `errorMode`:\n\n### Reveal Mode (Default)\n\nReturns **403** with a structured error:\n\n```json\n{\n \"error\": \"Record not found or not accessible\",\n \"layer\": \"firewall\",\n \"code\": \"FIREWALL_NOT_FOUND\",\n \"hint\": \"Check the record ID and your organization membership\"\n}\n```\n\nThis is developer-friendly and helps debug access issues during development.\n\n### Hide Mode\n\nReturns **404** with a generic error:\n\n```json\n{\n \"error\": \"Not found\",\n \"code\": \"NOT_FOUND\"\n}\n```\n\nUse `errorMode: 'hide'` for security-hardened deployments where you don't want attackers to distinguish between \"record exists but you can't access it\" and \"record doesn't exist\":\n\n```typescript\nfirewall: {\n organization: {},\n errorMode: 'hide', // Opaque 404s prevent record enumeration\n}\n```\n\n## Rules\n\n- Every resource MUST have at least one ownership scope OR `exception: true`\n- Cannot mix `exception: true` with ownership scopes\n\n## Why Can't You Mix `exception` with Ownership?\n\nThey represent opposite intentions:\n- `exception: true` = \"Generate NO WHERE clauses, data is public/global\"\n- Ownership scopes = \"Generate WHERE clauses to filter data\"\n\nCombining them would be contradictory - you can't both filter and not filter.\n\n## Handling \"Some Public, Some Private\" Data\n\nIf you need records that are sometimes public and sometimes scoped, you have two options:\n\n### Option 1: Two Separate Tables\n\nSplit into two tables - one public, one scoped:\n\n```typescript\n// features/templates/template-library.ts - Public templates\nexport default defineTable(templateLibrary, {\n firewall: { exception: true },\n // ...\n});\n\n// features/templates/user-templates.ts - User's custom templates\nexport default defineTable(userTemplates, {\n firewall: { owner: {} },\n // ...\n});\n```\n\n### Option 2: Use Access Control Instead\n\nKeep ownership scope but make access permissive, then control visibility via `access`:\n\n```typescript\n// features/documents/documents.ts\nexport default defineTable(documents, {\n firewall: {\n organization: {}, // Still scoped to org\n },\n crud: {\n list: {\n // Anyone in the org can list, but they see different things\n // based on a \"visibility\" field you check in your app logic\n access: { roles: [\"owner\", \"admin\", \"member\"] },\n },\n get: {\n // Use record conditions to allow public docs OR owned docs\n access: {\n or: [\n { record: { visibility: { equals: \"public\" } } },\n { record: { ownerId: { equals: \"$ctx.userId\" } } },\n { roles: [\"admin\"] }\n ]\n }\n },\n },\n // ...\n});\n```\n\n### Which to Choose?\n\n| Scenario | Recommendation |\n|----------|----------------|\n| Truly global data (app config, public templates) | `exception: true` in separate resource |\n| \"Public within org\" but still org-isolated | Ownership scope + permissive access rules |\n| User can toggle their own data public/private | Ownership scope + `visibility` field + access conditions |"
162
+ "content": "The firewall generates WHERE clauses automatically to isolate data by user, organization, or team.\n\n## Basic Usage\n\n```typescript\n// features/candidates/candidates.ts\n\nexport const candidates = sqliteTable('candidates', {\n id: text('id').primaryKey(),\n name: text('name').notNull(),\n email: text('email').notNull(),\n organizationId: text('organization_id').notNull(),\n});\n\nexport default defineTable(candidates, {\n firewall: { organization: {} }, // Isolate by organization\n // ... guards, crud\n});\n```\n\n## Configuration Options\n\n```typescript\nfirewall: {\n // User-level ownership (personal data)\n owner?: {\n column?: string; // Default: 'ownerId' or 'owner_id'\n source?: string; // Default: 'ctx.userId'\n mode?: 'required' | 'optional'; // Default: 'required'\n };\n\n // Organization-level ownership\n organization?: {\n column?: string; // Default: 'organizationId' or 'organization_id'\n source?: string; // Default: 'ctx.activeOrgId'\n };\n\n // Team-level ownership\n team?: {\n column?: string; // Default: 'teamId' or 'team_id'\n source?: string; // Default: 'ctx.activeTeamId'\n };\n\n // Soft delete filtering\n softDelete?: {\n column?: string; // Default: 'deletedAt'\n };\n\n // Error reporting mode for firewall violations\n // 'reveal' (default): 403 with structured error message\n // 'hide': Generic 404 (prevents record enumeration)\n errorMode?: 'reveal' | 'hide';\n\n // Opt-out for public tables (cannot combine with ownership)\n exception?: boolean;\n}\n```\n\n## Context Sources\n\nThe firewall pulls ownership values from the request context:\n\n| Scope | Default Source | Description |\n|-------|----------------|-------------|\n| `owner` | `ctx.userId` | Current authenticated user's ID |\n| `organization` | `ctx.activeOrgId` | User's active organization ID |\n| `team` | `ctx.activeTeamId` | User's active team ID |\n\nThese context values are populated by the auth middleware from the user's session.\n```\n\n## Auto-Detection\n\nQuickback automatically detects firewall scope based on your column names:\n\n| Column Name | Detected Scope |\n|-------------|----------------|\n| `organizationId` or `organization_id` | `organization` |\n| `ownerId` or `owner_id` | `owner` |\n| `teamId` or `team_id` | `team` |\n| `deletedAt` or `deleted_at` | `softDelete` |\n\nThis means you often don't need to specify column names:\n\n```typescript\n// Quickback detects organizationId column automatically\nfirewall: { organization: {} }\n\n// Quickback detects ownerId column automatically\nfirewall: { owner: {} }\n```\n\n## Common Patterns\n\n```typescript\n// Public/system table - no filtering\nfirewall: { exception: true }\n\n// Organization-scoped data (auto-detected from organizationId column)\nfirewall: { organization: {} }\n\n// Personal user data (auto-detected from ownerId column)\nfirewall: { owner: {} }\n\n// Org data with optional owner filtering\nfirewall: {\n organization: {},\n owner: { mode: 'optional' }\n}\n\n// With soft delete (auto-detected from deletedAt column)\nfirewall: {\n organization: {},\n softDelete: {}\n}\n```\n\n## Error Responses\n\nWhen a record isn't found behind the firewall (wrong org, soft-deleted, or genuinely doesn't exist), the error response depends on `errorMode`:\n\n### Reveal Mode (Default)\n\nReturns **403** with a structured error:\n\n```json\n{\n \"error\": \"Record not found or not accessible\",\n \"layer\": \"firewall\",\n \"code\": \"FIREWALL_NOT_FOUND\",\n \"hint\": \"Check the record ID and your organization membership\"\n}\n```\n\nThis is developer-friendly and helps debug access issues during development.\n\n### Hide Mode\n\nReturns **404** with a generic error:\n\n```json\n{\n \"error\": \"Not found\",\n \"code\": \"NOT_FOUND\"\n}\n```\n\nUse `errorMode: 'hide'` for security-hardened deployments where you don't want attackers to distinguish between \"record exists but you can't access it\" and \"record doesn't exist\":\n\n```typescript\nfirewall: {\n organization: {},\n errorMode: 'hide', // Opaque 404s prevent record enumeration\n}\n```\n\n## Rules\n\n- Every resource MUST have at least one ownership scope OR `exception: true`\n- Cannot mix `exception: true` with ownership scopes\n\n## Why Can't You Mix `exception` with Ownership?\n\nThey represent opposite intentions:\n- `exception: true` = \"Generate NO WHERE clauses, data is public/global\"\n- Ownership scopes = \"Generate WHERE clauses to filter data\"\n\nCombining them would be contradictory - you can't both filter and not filter.\n\n## Handling \"Some Public, Some Private\" Data\n\nIf you need records that are sometimes public and sometimes scoped, you have two options:\n\n### Option 1: Two Separate Tables\n\nSplit into two tables - one public, one scoped:\n\n```typescript\n// features/job-templates/template-library.ts - Public job templates\nexport default defineTable(templateLibrary, {\n firewall: { exception: true },\n // ...\n});\n\n// features/job-templates/custom-templates.ts - Org's custom job templates\nexport default defineTable(customTemplates, {\n firewall: { owner: {} },\n // ...\n});\n```\n\n### Option 2: Use Access Control Instead\n\nKeep ownership scope but make access permissive, then control visibility via `access`:\n\n```typescript\n// features/jobs/jobs.ts\nexport default defineTable(jobs, {\n firewall: {\n organization: {}, // Still scoped to org\n },\n crud: {\n list: {\n // Anyone in the org can list, but they see different things\n // based on a \"visibility\" field you check in your app logic\n access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\", \"interviewer\"] },\n },\n get: {\n // Use record conditions to allow public jobs OR owned jobs\n access: {\n or: [\n { record: { visibility: { equals: \"public\" } } },\n { record: { ownerId: { equals: \"$ctx.userId\" } } },\n { roles: [\"hiring-manager\"] }\n ]\n }\n },\n },\n // ...\n});\n```\n\n### Which to Choose?\n\n| Scenario | Recommendation |\n|----------|----------------|\n| Truly global data (app config, public job templates) | `exception: true` in separate resource |\n| \"Public within org\" but still org-isolated | Ownership scope + permissive access rules |\n| Recruiter can toggle job postings public/private | Ownership scope + `visibility` field + access conditions |"
159
163
  },
160
164
  "compiler/definitions/guards": {
161
165
  "title": "Guards - Field Modification Rules",
162
- "content": "Control which fields can be modified in CREATE vs UPDATE operations.\n\n## Basic Usage\n\n```typescript\n// features/invoices/invoices.ts\n\nexport const invoices = sqliteTable('invoices', {\n id: text('id').primaryKey(),\n name: text('name').notNull(),\n amount: integer('amount').notNull(),\n status: text('status').notNull().default('draft'),\n invoiceNumber: text('invoice_number'),\n organizationId: text('organization_id').notNull(),\n});\n\nexport default defineTable(invoices, {\n firewall: { organization: {} },\n guards: {\n createable: [\"name\", \"amount\", \"invoiceNumber\"],\n updatable: [\"name\"],\n protected: {\n status: [\"approve\", \"reject\"],\n amount: [\"reviseAmount\"],\n },\n immutable: [\"invoiceNumber\"],\n },\n crud: {\n // ...\n },\n});\n```\n\n## Configuration Options\n\n```typescript\nguards: {\n // Fields allowed on CREATE\n createable?: string[];\n\n // Fields allowed on UPDATE/PATCH\n updatable?: string[];\n\n // Fields only modifiable via specific actions\n protected?: Record<string, string[]>;\n\n // Fields set on CREATE, never modified after\n immutable?: string[];\n}\n```\n\n## How It Works\n\n| List | What it controls |\n|------|------------------|\n| `createable` | Fields allowed in create (POST) request body |\n| `updatable` | Fields allowed in update (PATCH) request body |\n| `protected` | Fields blocked from CRUD, only modifiable via named actions |\n| `immutable` | Fields allowed on create, blocked on all updates |\n\n**Combining lists:**\n\n- `createable` + `updatable` - Most fields go in both (can set on create AND modify later)\n- `createable` only - Field is set once, cannot be changed via update\n- `protected` - Don't also list in `createable` or `updatable` (they're mutually exclusive)\n- `immutable` - Don't also list in `updatable` (contradiction)\n\n```typescript\nguards: {\n createable: [\"name\", \"description\", \"category\"],\n updatable: [\"name\", \"description\"],\n // \"category\" is only in createable = set once, can't change via update\n protected: {\n status: [\"approve\", \"reject\"], // NOT in createable/updatable\n },\n immutable: [\"invoiceNumber\"], // NOT in updatable\n}\n```\n\n**If a field is not listed anywhere, it cannot be set by the client.**\n\n## System-Managed Fields (Always Protected)\n\nThese are automatically protected - you cannot override:\n- `createdAt`, `createdBy`\n- `modifiedAt`, `modifiedBy`\n- `deletedAt`, `deletedBy`\n\n## Example\n\n```typescript\nguards: {\n createable: [\"name\", \"description\", \"amount\"],\n updatable: [\"name\", \"description\"],\n protected: {\n status: [\"approve\", \"reject\"], // Only via these actions\n amount: [\"reviseAmount\"],\n },\n immutable: [\"invoiceNumber\"],\n}\n```\n\n## Disabling Guards\n\n```typescript\nguards: false // Only system fields protected\n```\n\nWhen `guards: false` is set, all user-defined fields become writable via CRUD operations. This is useful for:\n- External sync scenarios where you need full field control\n- Batch upsert operations where field restrictions would be limiting\n- Simple tables where field-level protection isn't needed\n\n## PUT/Upsert with External IDs\n\nWhen you disable guards AND use client-provided IDs, you unlock PUT (upsert) operations. This is designed for **syncing data from external systems**.\n\n### Requirements for PUT\n\n1. `generateId: false` in database config (client provides IDs)\n2. `guards: false` in resource definition\n\n### How PUT Works\n\n```\nPUT /resource/:id\n├── Record exists? → UPDATE (replace all fields)\n└── Record missing? → CREATE with provided ID\n```\n\n### Database Config\n\n```typescript\n// quickback.config.ts\nexport default {\n database: {\n generateId: false, // Client provides IDs (enables PUT)\n // Other options: 'uuid' | 'cuid' | 'nanoid' | 'prefixed' | 'serial'\n }\n};\n```\n\n### Table Definition\n\n```typescript\n// features/external/external-accounts.ts\nexport default defineTable(externalAccounts, {\n firewall: {\n organization: {}, // Still isolated by org\n },\n guards: false, // Disables field restrictions\n crud: {\n put: {\n access: { roles: ['admin', 'sync-service'] }\n }\n },\n});\n```\n\n## What's Still Protected with guards: false\n\nEven with `guards: false`, system-managed fields are ALWAYS protected:\n- `createdAt`, `createdBy` - Set on INSERT only\n- `modifiedAt`, `modifiedBy` - Auto-updated\n- `deletedAt`, `deletedBy` - Set on soft delete\n\n## Ownership Auto-Population\n\nWhen PUT creates a new record, ownership fields are auto-set from context:\n\n```typescript\n// Client sends: PUT /accounts/ext-123 { name: \"Acme\" }\n// Server creates:\n{\n id: \"ext-123\", // Client-provided\n name: \"Acme\", // Client-provided\n organizationId: ctx.activeOrgId, // Auto-set from firewall\n createdAt: now, // Auto-set\n createdBy: ctx.userId, // Auto-set\n modifiedAt: now, // Auto-set\n modifiedBy: ctx.userId, // Auto-set\n}\n```\n\n## Use Cases for PUT/External IDs\n\n| Use Case | Why PUT? |\n|----------|----------|\n| External API sync | External system controls the ID |\n| Webhook handlers | Events come with their own IDs |\n| Data migration | Preserve IDs from source system |\n| Idempotent updates | Safe to retry (no duplicate creates) |\n| Bulk upsert | Create or update in one operation |\n\n## ID Generation Options\n\n| `generateId` | PUT Available? | Notes |\n|--------------|----------------|-------|\n| `'uuid'` | No | Server generates UUID |\n| `'cuid'` | No | Server generates CUID |\n| `'nanoid'` | No | Server generates nanoid |\n| `'prefixed'` | No | Server generates prefixed ID (e.g. `room_abc123`) |\n| `'serial'` | No | Database auto-increments |\n| `false` | Yes (if guards: false) | Client provides ID |\n\n## Compile-Time Validation\n\nThe Quickback compiler validates your guards configuration and will error if:\n\n1. **Field in both `createable` and `protected`** - A field cannot be both client-writable on create and action-only\n2. **Field in both `updatable` and `protected`** - A field cannot be both client-writable on update and action-only\n3. **Field in both `updatable` and `immutable`** - Contradictory: immutable fields cannot be updated\n4. **Field in `protected` doesn't exist in schema** - Referenced field must exist in the table\n5. **Field in `createable`/`updatable`/`immutable` doesn't exist in schema** - All referenced fields must exist"
166
+ "content": "Control which fields can be modified in CREATE vs UPDATE operations.\n\n## Basic Usage\n\n```typescript\n// features/applications/applications.ts\n\nexport const applications = sqliteTable('applications', {\n id: text('id').primaryKey(),\n candidateId: text('candidate_id').notNull(),\n jobId: text('job_id').notNull(),\n stage: text('stage').notNull().default('applied'),\n appliedAt: text('applied_at').notNull(),\n notes: text('notes'),\n organizationId: text('organization_id').notNull(),\n});\n\nexport default defineTable(applications, {\n firewall: { organization: {} },\n guards: {\n createable: [\"candidateId\", \"jobId\", \"notes\"],\n updatable: [\"notes\"],\n protected: {\n stage: [\"advance-stage\", \"reject\"],\n },\n immutable: [\"appliedAt\", \"candidateId\", \"jobId\"],\n },\n crud: {\n // ...\n },\n});\n```\n\n## Configuration Options\n\n```typescript\nguards: {\n // Fields allowed on CREATE\n createable?: string[];\n\n // Fields allowed on UPDATE/PATCH\n updatable?: string[];\n\n // Fields only modifiable via specific actions\n protected?: Record<string, string[]>;\n\n // Fields set on CREATE, never modified after\n immutable?: string[];\n}\n```\n\n## How It Works\n\n| List | What it controls |\n|------|------------------|\n| `createable` | Fields allowed in create (POST) request body |\n| `updatable` | Fields allowed in update (PATCH) request body |\n| `protected` | Fields blocked from CRUD, only modifiable via named actions |\n| `immutable` | Fields allowed on create, blocked on all updates |\n\n**Combining lists:**\n\n- `createable` + `updatable` - Most fields go in both (can set on create AND modify later)\n- `createable` only - Field is set once, cannot be changed via update\n- `protected` - Don't also list in `createable` or `updatable` (they're mutually exclusive)\n- `immutable` - Don't also list in `updatable` (contradiction)\n\n```typescript\nguards: {\n createable: [\"candidateId\", \"jobId\", \"notes\", \"source\"],\n updatable: [\"notes\"],\n // \"candidateId\", \"jobId\" are only in createable = set once, can't change via update\n protected: {\n stage: [\"advance-stage\", \"reject\"], // NOT in createable/updatable\n },\n immutable: [\"appliedAt\"], // NOT in updatable\n}\n```\n\n**If a field is not listed anywhere, it cannot be set by the client.**\n\n## System-Managed Fields (Always Protected)\n\nThese are automatically protected - you cannot override:\n- `createdAt`, `createdBy`\n- `modifiedAt`, `modifiedBy`\n- `deletedAt`, `deletedBy`\n\n## Example\n\n```typescript\nguards: {\n createable: [\"candidateId\", \"jobId\", \"notes\"],\n updatable: [\"notes\"],\n protected: {\n stage: [\"advance-stage\", \"reject\"], // Only these actions can modify stage\n },\n immutable: [\"appliedAt\", \"candidateId\", \"jobId\"],\n}\n```\n\n## Disabling Guards\n\n```typescript\nguards: false // Only system fields protected\n```\n\nWhen `guards: false` is set, all user-defined fields become writable via CRUD operations. This is useful for:\n- External sync scenarios where you need full field control\n- Batch upsert operations where field restrictions would be limiting\n- Simple tables where field-level protection isn't needed\n\n## PUT/Upsert with External IDs\n\nWhen you disable guards AND use client-provided IDs, you unlock PUT (upsert) operations. This is designed for **syncing data from external systems**.\n\n### Requirements for PUT\n\n1. `generateId: false` in database config (client provides IDs)\n2. `guards: false` in resource definition\n\n### How PUT Works\n\n```\nPUT /resource/:id\n├── Record exists? → UPDATE (replace all fields)\n└── Record missing? → CREATE with provided ID\n```\n\n### Database Config\n\n```typescript\n// quickback.config.ts\nexport default {\n database: {\n generateId: false, // Client provides IDs (enables PUT)\n // Other options: 'uuid' | 'cuid' | 'nanoid' | 'prefixed' | 'serial'\n }\n};\n```\n\n### Table Definition\n\n```typescript\n// features/integrations/ats-imports.ts\nexport default defineTable(atsImports, {\n firewall: {\n organization: {}, // Still isolated by org\n },\n guards: false, // Disables field restrictions\n crud: {\n put: {\n access: { roles: ['hiring-manager', 'sync-service'] }\n }\n },\n});\n```\n\n## What's Still Protected with guards: false\n\nEven with `guards: false`, system-managed fields are ALWAYS protected:\n- `createdAt`, `createdBy` - Set on INSERT only\n- `modifiedAt`, `modifiedBy` - Auto-updated\n- `deletedAt`, `deletedBy` - Set on soft delete\n\n## Ownership Auto-Population\n\nWhen PUT creates a new record, ownership fields are auto-set from context:\n\n```typescript\n// Client sends: PUT /ats-imports/ext-123 { candidateName: \"Jane Doe\" }\n// Server creates:\n{\n id: \"ext-123\", // Client-provided\n candidateName: \"Jane Doe\", // Client-provided\n organizationId: ctx.activeOrgId, // Auto-set from firewall\n createdAt: now, // Auto-set\n createdBy: ctx.userId, // Auto-set\n modifiedAt: now, // Auto-set\n modifiedBy: ctx.userId, // Auto-set\n}\n```\n\n## Use Cases for PUT/External IDs\n\n| Use Case | Why PUT? |\n|----------|----------|\n| External API sync | External system controls the ID |\n| Webhook handlers | Events come with their own IDs |\n| Data migration | Preserve IDs from source system |\n| Idempotent updates | Safe to retry (no duplicate creates) |\n| Bulk upsert | Create or update in one operation |\n\n## ID Generation Options\n\n| `generateId` | PUT Available? | Notes |\n|--------------|----------------|-------|\n| `'uuid'` | No | Server generates UUID |\n| `'cuid'` | No | Server generates CUID |\n| `'nanoid'` | No | Server generates nanoid |\n| `'prefixed'` | No | Server generates prefixed ID (e.g. `room_abc123`) |\n| `'serial'` | No | Database auto-increments |\n| `false` | Yes (if guards: false) | Client provides ID |\n\n## Compile-Time Validation\n\nThe Quickback compiler validates your guards configuration and will error if:\n\n1. **Field in both `createable` and `protected`** - A field cannot be both client-writable on create and action-only\n2. **Field in both `updatable` and `protected`** - A field cannot be both client-writable on update and action-only\n3. **Field in both `updatable` and `immutable`** - Contradictory: immutable fields cannot be updated\n4. **Field in `protected` doesn't exist in schema** - Referenced field must exist in the table\n5. **Field in `createable`/`updatable`/`immutable` doesn't exist in schema** - All referenced fields must exist"
163
167
  },
164
168
  "compiler/definitions": {
165
169
  "title": "Definitions Overview",
166
- "content": "Before diving into specific features, let's understand how Quickback's pieces connect. This page gives you the mental model for everything that follows.\n\n## The Big Picture\n\nQuickback is a **backend compiler**. You write definition files, and Quickback compiles them into a production-ready API.\n\n1. **You write definitions** - Table files with schema and security config using `defineTable`\n2. **Quickback compiles them** - Analyzes your definitions at build time\n3. **You get a production API** - `GET /rooms`, `POST /rooms`, `PATCH /rooms/:id`, `DELETE /rooms/:id`, batch operations, plus custom actions\n\n## File Structure\n\nYour definitions live in a `definitions/` folder organized by feature:\n\n```\nmy-app/\n├── quickback/\n│ ├── quickback.config.ts # Compiler configuration\n│ └── definitions/\n│ └── features/\n│ └── {feature-name}/\n│ ├── claims.ts # Table + config (defineTable)\n│ ├── claim-versions.ts # Secondary table + config\n│ ├── claim-sources.ts # Internal table (no routes)\n│ ├── actions.ts # Custom actions (optional)\n│ └── handlers/ # Action handlers (optional)\n│ └── my-action.ts\n├── src/ # Generated code (output)\n├── drizzle/ # Generated migrations\n└── package.json\n```\n\n**Table files** use `defineTable` to combine schema and security config:\n\n```typescript\n// features/claims/claims.ts\n\nexport const claims = sqliteTable(\"claims\", {\n id: text(\"id\").primaryKey(),\n organizationId: text(\"organization_id\").notNull(),\n content: text(\"content\").notNull(),\n});\n\nexport default defineTable(claims, {\n firewall: { organization: {} },\n guards: {\n createable: [\"content\"],\n updatable: [\"content\"],\n },\n crud: {\n list: { access: { roles: [\"owner\", \"admin\", \"member\"] } },\n get: { access: { roles: [\"owner\", \"admin\", \"member\"] } },\n create: { access: { roles: [\"owner\", \"admin\", \"member\"] } },\n update: { access: { roles: [\"owner\", \"admin\", \"member\"] } },\n delete: { access: { roles: [\"owner\", \"admin\"] } },\n },\n});\n\nexport type Claim = typeof claims.$inferSelect;\n```\n\n**Key points:**\n- Tables with `export default defineTable(...)` → CRUD routes generated\n- Tables without default export → internal/junction tables (no routes)\n- Route path derived from filename: `claim-versions.ts` → `/api/v1/claim-versions`\n\n## The Four Security Layers\n\nEvery API request passes through four security layers, in order:\n\n```\nRequest → Firewall → Access → Guards → Masking → Response\n │ │ │ │\n │ │ │ └── Hide sensitive fields\n │ │ └── Block field modifications\n │ └── Check roles & conditions\n └── Isolate data by owner/org/team\n```\n\n### 1. Firewall (Data Isolation)\n\nThe firewall controls **which records** a user can see. It automatically adds WHERE clauses to every query based on your schema columns:\n\n| Column in Schema | What happens |\n|------------------|--------------|\n| `organization_id` | Data isolated by organization |\n| `user_id` | Data isolated by user (personal data) |\n\nNo manual configuration needed - Quickback applies smart rules based on your schema.\n\n[Learn more about Firewall →](/compiler/definitions/firewall)\n\n### 2. Access (CRUD Permissions)\n\nAccess controls **which operations** a user can perform. It checks roles and record conditions.\n\n```typescript\ncrud: {\n list: { access: { roles: [\"owner\", \"admin\", \"member\"] } },\n create: { access: { roles: [\"owner\", \"admin\"] } },\n update: { access: { roles: [\"owner\", \"admin\"] } },\n delete: { access: { roles: [\"owner\", \"admin\"] } },\n}\n```\n\n[Learn more about Access →](/compiler/definitions/access)\n\n### 3. Guards (Field Modification Rules)\n\nGuards control **which fields** can be modified in each operation.\n\n| Guard Type | What it means |\n|------------|---------------|\n| `createable` | Fields that can be set when creating |\n| `updatable` | Fields that can be changed when updating |\n| `protected` | Fields that can only be changed via specific actions |\n| `immutable` | Fields that can never be changed after creation |\n\n[Learn more about Guards →](/compiler/definitions/guards)\n\n### 4. Masking (Data Redaction)\n\nMasking hides sensitive fields from users who shouldn't see them.\n\n```typescript\nmasking: {\n ssn: { type: 'ssn' }, // Shows: *****1234\n email: { type: 'email' }, // Shows: j***@y*********.com\n salary: { type: 'redact' }, // Shows: [REDACTED]\n}\n```\n\n[Learn more about Masking →](/compiler/definitions/masking)\n\n## How They Work Together\n\n**Scenario:** A member requests `GET /employees/123`\n\n1. **Firewall** checks: Is employee 123 in the user's organization?\n - Yes → Continue\n - No → 404 Not Found (as if it doesn't exist)\n\n2. **Access** checks: Can members perform GET?\n - Yes → Continue\n - No → 403 Forbidden\n\n3. **Guards** don't apply to GET (they're for writes)\n\n4. **Masking** applies: User is a member, not admin\n - SSN: `123-45-6789` → `*****6789`\n - Salary: `85000` → `[REDACTED]`\n\n5. **Response** sent with masked data\n\n## Locked Down by Default\n\nQuickback is **secure by default**. Nothing is accessible until you explicitly allow it.\n\n| Layer | Default | What you must do |\n|-------|---------|------------------|\n| Firewall | AUTO | Auto-detects from `organization_id`/`user_id` columns. Only configure for exceptions. |\n| Access | DENIED | Explicitly define `access` rules with roles |\n| Guards | LOCKED | Explicitly list `createable`, `updatable` fields |\n| Actions | BLOCKED | Explicitly define `access` for each action |\n\n**You must deliberately open each door.** This prevents accidental data exposure.\n\n## Next Steps\n\n1. [Database Schema](/compiler/definitions/schema) — Define your tables\n2. [Firewall](/compiler/definitions/firewall) — Set up data isolation\n3. [Access](/compiler/definitions/access) — Configure CRUD permissions\n4. [Guards](/compiler/definitions/guards) — Control field modifications\n5. [Masking](/compiler/definitions/masking) — Hide sensitive data\n6. [Views](/compiler/definitions/views) — Column-level security\n7. [Validation](/compiler/definitions/validation) — Field validation rules\n8. [Actions](/compiler/definitions/actions) — Add custom business logic"
170
+ "content": "Before diving into specific features, let's understand how Quickback's pieces connect. This page gives you the mental model for everything that follows.\n\n## The Big Picture\n\nQuickback is a **backend compiler**. You write definition files, and Quickback compiles them into a production-ready API.\n\n1. **You write definitions** - Table files with schema and security config using `defineTable`\n2. **Quickback compiles them** - Analyzes your definitions at build time\n3. **You get a production API** - `GET /jobs`, `POST /jobs`, `PATCH /jobs/:id`, `DELETE /jobs/:id`, batch operations, plus custom actions\n\n## File Structure\n\nYour definitions live in a `quickback/features/` folder organized by feature:\n\n```\nmy-app/\n├── quickback/\n│ ├── quickback.config.ts # Compiler configuration\n│ └── features/\n│ └── {feature-name}/\n│ ├── candidates.ts # Table + config (defineTable)\n│ ├── applications.ts # Secondary table + config\n│ ├── interview-scores.ts # Internal table (no routes)\n│ ├── actions.ts # Custom actions (optional)\n│ └── handlers/ # Action handlers (optional)\n│ └── advance-stage.ts\n├── src/ # Generated code (output)\n├── drizzle/ # Generated migrations\n└── package.json\n```\n\n**Table files** use `defineTable` to combine schema and security config:\n\n```typescript\n// features/candidates/candidates.ts\n\nexport const candidates = sqliteTable(\"candidates\", {\n id: text(\"id\").primaryKey(),\n organizationId: text(\"organization_id\").notNull(),\n name: text(\"name\").notNull(),\n email: text(\"email\").notNull(),\n phone: text(\"phone\"),\n source: text(\"source\"),\n});\n\nexport default defineTable(candidates, {\n firewall: { organization: {} },\n guards: {\n createable: [\"name\", \"email\", \"phone\", \"source\"],\n updatable: [\"name\", \"phone\"],\n },\n masking: {\n email: { type: 'email', show: { roles: ['owner', 'hiring-manager', 'recruiter'] } },\n phone: { type: 'phone', show: { roles: ['owner', 'hiring-manager', 'recruiter'] } },\n },\n crud: {\n list: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\", \"interviewer\"] } },\n get: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\", \"interviewer\"] } },\n create: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\"] } },\n update: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\"] } },\n delete: { access: { roles: [\"owner\", \"hiring-manager\"] } },\n },\n});\n\nexport type Candidate = typeof candidates.$inferSelect;\n```\n\n**Key points:**\n- Tables with `export default defineTable(...)` → CRUD routes generated\n- Tables without default export → internal/junction tables (no routes)\n- Route path derived from filename: `applications.ts` → `/api/v1/applications`\n\n## The Four Security Layers\n\nEvery API request passes through four security layers, in order:\n\n```\nRequest → Firewall → Access → Guards → Masking → Response\n │ │ │ │\n │ │ │ └── Hide sensitive fields\n │ │ └── Block field modifications\n │ └── Check roles & conditions\n └── Isolate data by owner/org/team\n```\n\n### 1. Firewall (Data Isolation)\n\nThe firewall controls **which records** a user can see. It automatically adds WHERE clauses to every query based on your schema columns:\n\n| Column in Schema | What happens |\n|------------------|--------------|\n| `organization_id` | Data isolated by organization |\n| `user_id` | Data isolated by user (personal data) |\n\nNo manual configuration needed - Quickback applies smart rules based on your schema.\n\n[Learn more about Firewall →](/compiler/definitions/firewall)\n\n### 2. Access (CRUD Permissions)\n\nAccess controls **which operations** a user can perform. It checks roles and record conditions.\n\n```typescript\ncrud: {\n list: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\", \"interviewer\"] } },\n create: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\"] } },\n update: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\"] } },\n delete: { access: { roles: [\"owner\", \"hiring-manager\"] } },\n}\n```\n\n[Learn more about Access →](/compiler/definitions/access)\n\n### 3. Guards (Field Modification Rules)\n\nGuards control **which fields** can be modified in each operation.\n\n| Guard Type | What it means |\n|------------|---------------|\n| `createable` | Fields that can be set when creating |\n| `updatable` | Fields that can be changed when updating |\n| `protected` | Fields that can only be changed via specific actions |\n| `immutable` | Fields that can never be changed after creation |\n\n[Learn more about Guards →](/compiler/definitions/guards)\n\n### 4. Masking (Data Redaction)\n\nMasking hides sensitive fields from users who shouldn't see them.\n\n```typescript\nmasking: {\n email: { type: 'email' }, // Shows: j***@e******.com\n phone: { type: 'phone' }, // Shows: ******4567\n salary: { type: 'redact' }, // Shows: [REDACTED]\n}\n```\n\n[Learn more about Masking →](/compiler/definitions/masking)\n\n## How They Work Together\n\n**Scenario:** An interviewer requests `GET /candidates/cnd_123`\n\n1. **Firewall** checks: Is candidate `cnd_123` in the user's organization?\n - Yes → Continue\n - No → 404 Not Found (as if it doesn't exist)\n\n2. **Access** checks: Can interviewers perform GET?\n - Yes → Continue\n - No → 403 Forbidden\n\n3. **Guards** don't apply to GET (they're for writes)\n\n4. **Masking** applies: User is an `interviewer`, not `recruiter` or `hiring-manager`\n - Email: `jane@company.com` → `j***@c******.com`\n - Phone: `555-123-4567` → `******4567`\n\n5. **Response** sent with masked data\n\n## Locked Down by Default\n\nQuickback is **secure by default**. Nothing is accessible until you explicitly allow it.\n\n| Layer | Default | What you must do |\n|-------|---------|------------------|\n| Firewall | AUTO | Auto-detects from `organization_id`/`user_id` columns. Only configure for exceptions. |\n| Access | DENIED | Explicitly define `access` rules with roles |\n| Guards | LOCKED | Explicitly list `createable`, `updatable` fields |\n| Actions | BLOCKED | Explicitly define `access` for each action |\n\n**You must deliberately open each door.** This prevents accidental data exposure.\n\n## Next Steps\n\n1. [Database Schema](/compiler/definitions/schema) — Define your tables\n2. [Firewall](/compiler/definitions/firewall) — Set up data isolation\n3. [Access](/compiler/definitions/access) — Configure CRUD permissions\n4. [Guards](/compiler/definitions/guards) — Control field modifications\n5. [Masking](/compiler/definitions/masking) — Hide sensitive data\n6. [Views](/compiler/definitions/views) — Column-level security\n7. [Validation](/compiler/definitions/validation) — Field validation rules\n8. [Actions](/compiler/definitions/actions) — Add custom business logic"
167
171
  },
168
172
  "compiler/definitions/masking": {
169
173
  "title": "Masking - Field Redaction",
170
- "content": "Hide sensitive data from unauthorized users while showing it to those with permission.\n\n## Automatic Masking (Secure by Default)\n\nQuickback automatically applies masking to columns that match sensitive naming patterns. This ensures a high security posture even if you don't explicitly configure masking.\n\n### Sensitive Keywords & Default Masks\n\n| Pattern | Default Mask | Description |\n| :--- | :--- | :--- |\n| `email` | `email` | p***@e******.com |\n| `phone`, `mobile`, `fax` | `phone` | ******4567 |\n| `ssn`, `socialsecurity`, `nationalid` | `ssn` | *****6789 |\n| `creditcard`, `cc`, `cardnumber`, `cvv` | `creditCard` | ************1234 |\n| `iban` | `creditCard` | ************1234 |\n| `password`, `secret`, `token`, `apikey`, `privatekey` | `redact` | [REDACTED] |\n\n### Build-Time Alerts\n\nIf the compiler auto-detects a sensitive column that you haven't explicitly configured, it will emit a warning:\n\n```bash\n[Warning] Auto-masking enabled for sensitive column \"users.email\". Explicitly configure masking to silence this warning.\n```\n\nTo silence this warning or change the behavior, simply define the field explicitly in your `masking` configuration. Explicit configurations always take precedence over auto-detected defaults.\n\n### Overriding Defaults\n\nIf you want to show a sensitive field to everyone (disable masking) or use a different rule, define it explicitly in the `resource.masking` block:\n\n```typescript\nexport default defineTable(users, {\n masking: {\n // Show email to everyone (overrides auto-masking)\n email: { type: 'email', show: { roles: ['everyone'] } },\n \n // Silence warning but keep secure (owner-only)\n ssn: { type: 'ssn', show: { or: 'owner' } }\n }\n});\n```\n\n## Basic Usage\n\n```typescript\n// features/employees/employees.ts\n\nexport const employees = sqliteTable('employees', {\n id: text('id').primaryKey(),\n name: text('name').notNull(),\n ssn: text('ssn'),\n salary: integer('salary'),\n email: text('email'),\n organizationId: text('organization_id').notNull(),\n});\n\nexport default defineTable(employees, {\n firewall: { organization: {} },\n guards: { createable: [\"name\", \"ssn\", \"salary\", \"email\"], updatable: [\"name\"] },\n masking: {\n ssn: { type: 'ssn', show: { roles: ['hr', 'admin'] } },\n salary: { type: 'redact', show: { roles: ['hr', 'admin'] } },\n email: { type: 'email', show: { or: 'owner' } },\n },\n crud: {\n // ...\n },\n});\n```\n\n## Built-in Mask Types\n\n| Type | Example Input | Masked Output |\n|------|---------------|---------------|\n| `'email'` | `john@yourdomain.com` | `j***@y*********.com` |\n| `'phone'` | `555-123-4567` | `******4567` |\n| `'ssn'` | `123-45-6789` | `*****6789` |\n| `'creditCard'` | `4111111111111111` | `************1111` |\n| `'name'` | `John Smith` | `J*** S****` |\n| `'redact'` | `anything` | `[REDACTED]` |\n| `'custom'` | (your logic) | (your output) |\n\n## Configuration\n\n```typescript\nmasking: {\n // Basic masking - everyone sees masked value\n taxId: { type: 'ssn' },\n\n // Show unmasked to specific roles\n salary: {\n type: 'redact',\n show: { roles: ['admin', 'hr'] }\n },\n\n // Show unmasked to owner (createdBy === ctx.userId)\n email: {\n type: 'email',\n show: { or: 'owner' }\n },\n\n // Custom mask function\n apiKey: {\n type: 'custom',\n mask: (value) => value.slice(0, 4) + '...' + value.slice(-4),\n show: { roles: ['admin'] }\n },\n}\n```\n\n## Show Conditions\n\n```typescript\nshow: {\n roles?: string[]; // Unmasked if user has any of these roles\n or?: 'owner'; // Unmasked if user is the record owner\n}\n```\n\nThe `'owner'` condition compares against the owner column configured in your firewall. If you have `firewall: { owner: {} }`, the owner column (default: `ownerId` or `owner_id`) is used to determine ownership. If no owner firewall is configured, it falls back to `createdBy`.\n\n## Complete Example\n\n```typescript\n// features/employees/employees.ts\nexport default defineTable(employees, {\n firewall: { organization: {} },\n guards: { createable: [\"name\", \"ssn\", \"salary\"], updatable: [\"name\"] },\n masking: {\n ssn: { type: 'ssn', show: { roles: ['hr', 'admin'] } },\n salary: { type: 'redact', show: { roles: ['hr', 'admin'] } },\n personalEmail: { type: 'email', show: { or: 'owner' } },\n bankAccount: {\n type: 'custom',\n mask: (val) => '****' + val.slice(-4),\n show: { roles: ['payroll'] }\n },\n },\n crud: {\n list: { access: { roles: [\"member\", \"admin\"] } },\n get: { access: { roles: [\"member\", \"admin\"] } },\n create: { access: { roles: [\"hr\", \"admin\"] } },\n update: { access: { roles: [\"hr\", \"admin\"] } },\n delete: { access: { roles: [\"admin\"] } },\n },\n});\n```"
174
+ "content": "Hide sensitive data from unauthorized users while showing it to those with permission.\n\n## Automatic Masking (Secure by Default)\n\nQuickback automatically applies masking to columns that match sensitive naming patterns. This ensures a high security posture even if you don't explicitly configure masking.\n\n### Sensitive Keywords & Default Masks\n\n| Pattern | Default Mask | Description |\n| :--- | :--- | :--- |\n| `email` | `email` | p***@e******.com |\n| `phone`, `mobile`, `fax` | `phone` | ******4567 |\n| `ssn`, `socialsecurity`, `nationalid` | `ssn` | *****6789 |\n| `creditcard`, `cc`, `cardnumber`, `cvv` | `creditCard` | ************1234 |\n| `iban` | `creditCard` | ************1234 |\n| `password`, `secret`, `token`, `apikey`, `privatekey` | `redact` | [REDACTED] |\n\n### Build-Time Alerts\n\nIf the compiler auto-detects a sensitive column that you haven't explicitly configured, it will emit a warning:\n\n```bash\n[Warning] Auto-masking enabled for sensitive column \"users.email\". Explicitly configure masking to silence this warning.\n```\n\nTo silence this warning or change the behavior, simply define the field explicitly in your `masking` configuration. Explicit configurations always take precedence over auto-detected defaults.\n\n### Overriding Defaults\n\nIf you want to show a sensitive field to everyone (disable masking) or use a different rule, define it explicitly in the `resource.masking` block:\n\n```typescript\nexport default defineTable(candidates, {\n masking: {\n // Show email to everyone (overrides auto-masking)\n email: { type: 'email', show: { roles: ['everyone'] } },\n\n // Silence warning but keep secure (recruiter and above only)\n phone: { type: 'phone', show: { roles: ['owner', 'hiring-manager', 'recruiter'] } }\n }\n});\n```\n\n## Basic Usage\n\n```typescript\n// features/candidates/candidates.ts\n\nexport const candidates = sqliteTable('candidates', {\n id: text('id').primaryKey(),\n name: text('name').notNull(),\n email: text('email').notNull(),\n phone: text('phone'),\n resumeUrl: text('resume_url'),\n organizationId: text('organization_id').notNull(),\n});\n\nexport default defineTable(candidates, {\n firewall: { organization: {} },\n guards: { createable: [\"name\", \"email\", \"phone\", \"resumeUrl\"], updatable: [\"name\", \"phone\"] },\n masking: {\n email: { type: 'email', show: { roles: ['owner', 'hiring-manager', 'recruiter'] } },\n phone: { type: 'phone', show: { roles: ['owner', 'hiring-manager', 'recruiter'] } },\n },\n crud: {\n // ...\n },\n});\n```\n\n## Built-in Mask Types\n\n| Type | Example Input | Masked Output |\n|------|---------------|---------------|\n| `'email'` | `john@yourdomain.com` | `j***@y*********.com` |\n| `'phone'` | `555-123-4567` | `******4567` |\n| `'ssn'` | `123-45-6789` | `*****6789` |\n| `'creditCard'` | `4111111111111111` | `************1111` |\n| `'name'` | `John Smith` | `J*** S****` |\n| `'redact'` | `anything` | `[REDACTED]` |\n| `'custom'` | (your logic) | (your output) |\n\n## Configuration\n\n```typescript\nmasking: {\n // Basic masking - everyone sees masked value\n phone: { type: 'phone' },\n\n // Show unmasked to specific roles\n email: {\n type: 'email',\n show: { roles: ['hiring-manager', 'recruiter'] }\n },\n\n // Show unmasked to owner (createdBy === ctx.userId)\n resumeUrl: {\n type: 'redact',\n show: { or: 'owner' }\n },\n\n // Custom mask function\n externalProfileUrl: {\n type: 'custom',\n mask: (value) => value.slice(0, 4) + '...' + value.slice(-4),\n show: { roles: ['recruiter'] }\n },\n}\n```\n\n## Show Conditions\n\n```typescript\nshow: {\n roles?: string[]; // Unmasked if user has any of these roles\n or?: 'owner'; // Unmasked if user is the record owner\n}\n```\n\nThe `'owner'` condition compares against the owner column configured in your firewall. If you have `firewall: { owner: {} }`, the owner column (default: `ownerId` or `owner_id`) is used to determine ownership. If no owner firewall is configured, it falls back to `createdBy`.\n\n## Complete Example\n\n```typescript\n// features/candidates/candidates.ts\nexport default defineTable(candidates, {\n firewall: { organization: {} },\n guards: { createable: [\"name\", \"email\", \"phone\", \"source\"], updatable: [\"name\", \"phone\"] },\n masking: {\n email: { type: 'email', show: { roles: ['hiring-manager', 'recruiter'] } },\n phone: { type: 'phone', show: { roles: ['hiring-manager', 'recruiter'] } },\n resumeUrl: {\n type: 'custom',\n mask: (val) => '[RESUME LINK HIDDEN]',\n show: { roles: ['hiring-manager', 'recruiter'] }\n },\n },\n crud: {\n list: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\", \"interviewer\"] } },\n get: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\", \"interviewer\"] } },\n create: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\"] } },\n update: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\"] } },\n delete: { access: { roles: [\"owner\", \"hiring-manager\"] } },\n },\n});\n```"
171
175
  },
172
176
  "compiler/definitions/schema": {
173
177
  "title": "Database Schema",
174
- "content": "Quickback uses [Drizzle ORM](https://orm.drizzle.team/) to define your database schema. With `defineTable`, you combine your schema definition and security configuration in a single file.\n\n## Defining Tables with defineTable\n\nEach table gets its own file with schema and config together. Use the Drizzle dialect that matches your target database:\n\n| Target Database | Import From | Table Function |\n|-----------------|-------------|----------------|\n| Cloudflare D1, Turso, SQLite | `drizzle-orm/sqlite-core` | `sqliteTable` |\n| Supabase, Neon, PostgreSQL | `drizzle-orm/pg-core` | `pgTable` |\n| PlanetScale, MySQL | `drizzle-orm/mysql-core` | `mysqlTable` |\n\n```typescript\n// definitions/features/rooms/rooms.ts\n// For D1/SQLite targets:\n\nexport const rooms = sqliteTable('rooms', {\n id: text('id').primaryKey(),\n name: text('name').notNull(),\n description: text('description'),\n capacity: integer('capacity').notNull().default(10),\n roomType: text('room_type').notNull(),\n isActive: integer('is_active', { mode: 'boolean' }).notNull().default(true),\n\n // Ownership - for firewall data isolation\n organizationId: text('organization_id').notNull(),\n});\n\nexport default defineTable(rooms, {\n firewall: { organization: {} },\n guards: {\n createable: [\"name\", \"description\", \"capacity\", \"roomType\"],\n updatable: [\"name\", \"description\", \"capacity\"],\n },\n crud: {\n list: { access: { roles: [\"owner\", \"admin\", \"member\"] } },\n get: { access: { roles: [\"owner\", \"admin\", \"member\"] } },\n create: { access: { roles: [\"owner\", \"admin\"] } },\n update: { access: { roles: [\"owner\", \"admin\"] } },\n delete: { access: { roles: [\"owner\", \"admin\"] } },\n },\n});\n\nexport type Room = typeof rooms.$inferSelect;\n```\n\n## Column Types\n\nDrizzle supports all standard SQL column types:\n\n| Type | Drizzle Function | Example |\n|------|------------------|---------|\n| String | `text()`, `varchar()` | `text('name')` |\n| Integer | `integer()`, `bigint()` | `integer('count')` |\n| Boolean | `boolean()` | `boolean('is_active')` |\n| Timestamp | `timestamp()` | `timestamp('created_at')` |\n| JSON | `json()`, `jsonb()` | `jsonb('metadata')` |\n| UUID | `uuid()` | `uuid('id')` |\n| Decimal | `decimal()`, `numeric()` | `decimal('price', { precision: 10, scale: 2 })` |\n\n## Column Modifiers\n\n```typescript\n// Required field\nname: text('name').notNull()\n\n// Default value\nisActive: boolean('is_active').default(true)\n\n// Primary key\nid: text('id').primaryKey()\n\n// Unique constraint\nemail: text('email').unique()\n\n// Default to current timestamp\ncreatedAt: timestamp('created_at').defaultNow()\n```\n\n## File Organization\n\nOrganize your tables by feature. Each feature directory contains table files:\n\n```\ndefinitions/\n└── features/\n ├── rooms/\n │ ├── rooms.ts # Main table + config\n │ ├── room-bookings.ts # Related table + config\n │ └── actions.ts # Custom actions\n ├── users/\n │ ├── users.ts # Table + config\n │ └── user-preferences.ts # Related table\n └── organizations/\n └── organizations.ts\n```\n\n**Key points:**\n- Tables with `export default defineTable(...)` get CRUD routes generated\n- Tables without a default export are internal (no routes, used for joins/relations)\n- Route paths are derived from filenames: `room-bookings.ts` → `/api/v1/room-bookings`\n\n### defineTable vs defineResource\n\nBoth functions are available:\n\n- **`defineTable`** - The standard function for defining tables with CRUD routes\n- **`defineResource`** - Alias for `defineTable`, useful when thinking in terms of REST resources\n\n```typescript\n// These are equivalent:\nexport default defineTable(rooms, { /* config */ });\nexport default defineResource(rooms, { /* config */ });\n```\n\n## 1 Resource = 1 Security Boundary\n\nEach `defineTable()` call defines a complete, self-contained security boundary. The security config you write — firewall, access, guards, and masking — is compiled into a single resource file that wraps all CRUD routes for that table.\n\nThis is a deliberate design choice. Mixing two resources with different security rules in one configuration would create ambiguity about which firewall, access, or masking rules apply to which table. By keeping it 1:1, there's never any question.\n\n| Scenario | What to do |\n|----------|------------|\n| Table needs its own API routes + security | Own file with `defineTable()` |\n| Table is internal/supporting (no direct API) | Extra `.ts` file in the parent feature directory, no `defineTable()` |\n\nA supporting table without `defineTable()` is useful when it's accessed internally — by action handlers, joins, or background jobs — but should never be directly exposed as its own API endpoint.\n\n## Internal Tables (No Routes)\n\nFor junction tables or internal data structures that shouldn't have API routes, simply omit the `defineTable` export:\n\n```typescript\n// definitions/features/rooms/room-amenities.ts\n\n// Junction table - no routes needed\nexport const roomAmenities = sqliteTable('room_amenities', {\n roomId: text('room_id').notNull(),\n amenityId: text('amenity_id').notNull(),\n});\n\n// No default export = no CRUD routes generated\n```\n\nThese internal tables still participate in the database schema and migrations — they just don't get API routes or security configuration.\n\nThe compiler automatically injects **audit fields** (`createdAt`, `modifiedAt`, `deletedAt`, etc.) and **`organizationId`** into child/junction tables when the parent feature is org-scoped. This ensures cascade soft deletes and scoped queries work correctly without manual column definitions.\n\n## Audit Fields\n\nQuickback automatically adds and manages these audit fields on **all tables** in a feature (including child/junction tables without `defineTable`) - you don't need to define them:\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `createdAt` | timestamp | Set when record is created |\n| `createdBy` | text | User ID who created the record |\n| `modifiedAt` | timestamp | Updated on every change |\n| `modifiedBy` | text | User ID who last modified |\n| `deletedAt` | timestamp | Set on soft delete (optional) |\n| `deletedBy` | text | User ID who deleted (optional) |\n\nThe soft delete fields (`deletedAt`, `deletedBy`) are only added if your resource uses soft delete mode.\n\n### Disabling Audit Fields\n\nTo disable automatic audit fields for your project:\n\n```typescript\n// quickback.config.ts\nexport default defineConfig({\n // ...\n compiler: {\n features: {\n auditFields: false, // Disable for entire project\n }\n }\n});\n```\n\n### Protected System Fields\n\nThese fields are always protected and cannot be set by clients, even with `guards: false`:\n\n- `id` (when `generateId` is not `false`)\n- `createdAt`, `createdBy`\n- `modifiedAt`, `modifiedBy`\n- `deletedAt`, `deletedBy`\n\n## Ownership Fields\n\nFor the firewall to work, include the appropriate ownership columns:\n\n```typescript\n// For organization-scoped data (most common)\norganizationId: text('organization_id').notNull()\n\n// For user-owned data (personal data)\nownerId: text('owner_id').notNull()\n\n// For team-scoped data\nteamId: text('team_id').notNull()\n```\n\n## Display Column\n\nWhen a table has foreign key columns (e.g., `roomTypeId` referencing `roomTypes`), Quickback can automatically resolve the human-readable label in GET and LIST responses. This eliminates the need for frontend lookup calls.\n\n### Auto-Detection\n\nQuickback auto-detects the display column by checking for common column names in this order:\n\n`name` → `title` → `label` → `headline` → `subject` → `code` → `displayName` → `fullName` → `description`\n\nIf your `roomTypes` table has a `name` column, it's automatically used as the display column. No config needed.\n\n### Explicit Override\n\nOverride the auto-detected column with `displayColumn`:\n\n```typescript\nexport default defineTable(accountCodes, {\n displayColumn: 'code', // Use 'code' instead of auto-detected 'name'\n firewall: { organization: {} },\n crud: {\n list: { access: { roles: [\"member\", \"admin\"] } },\n get: { access: { roles: [\"member\", \"admin\"] } },\n },\n});\n```\n\n### How Labels Appear in Responses\n\nFor any FK column ending in `Id`, the API adds a `_label` field with the resolved display value:\n\n```json\n{\n \"id\": \"rm_abc\",\n \"name\": \"Main Conference Room\",\n \"roomTypeId\": \"rt_xyz\",\n \"roomType_label\": \"Conference\",\n \"accountCodeId\": \"ac_123\",\n \"accountCode_label\": \"Revenue\"\n}\n```\n\nThe pattern is `{columnWithoutId}_label`. The frontend can find all labels with:\n\n```typescript\nObject.keys(record).filter(k => k.endsWith('_label'))\n```\n\nLabel resolution works within the same feature (tables in the same feature directory). System columns (`organizationId`, `createdBy`, `modifiedBy`) are never resolved.\n\nFor LIST endpoints, labels are batch-resolved efficiently — one query per FK column, not per record.\n\n## Relations (Optional)\n\nDrizzle supports defining relations for type-safe joins:\n\n```typescript\n\nexport const roomsRelations = relations(rooms, ({ one, many }) => ({\n organization: one(organizations, {\n fields: [rooms.organizationId],\n references: [organizations.id],\n }),\n bookings: many(bookings),\n}));\n```\n\n## Database Configuration\n\nConfigure database options in your Quickback config:\n\n```typescript\n// quickback.config.ts\nexport default defineConfig({\n name: 'my-app',\n providers: {\n database: defineDatabase('cloudflare-d1', {\n generateId: 'prefixed', // 'uuid' | 'cuid' | 'nanoid' | 'prefixed' | 'serial' | false\n namingConvention: 'snake_case', // 'camelCase' | 'snake_case'\n usePlurals: false, // Auth table names: 'users' vs 'user'\n }),\n },\n compiler: {\n features: {\n auditFields: true, // Auto-manage audit timestamps\n }\n }\n});\n```\n\n## Choosing Your Dialect\n\nUse the Drizzle dialect that matches your database provider:\n\n### SQLite (D1, Turso, better-sqlite3)\n\n```typescript\n\nexport const posts = sqliteTable('posts', {\n id: text('id').primaryKey(),\n title: text('title').notNull(),\n metadata: text('metadata', { mode: 'json' }), // JSON stored as text\n isPublished: integer('is_published', { mode: 'boolean' }).default(false),\n organizationId: text('organization_id').notNull(),\n});\n```\n\n### PostgreSQL (Supabase, Neon)\n\n```typescript\n\nexport const posts = pgTable('posts', {\n id: serial('id').primaryKey(),\n title: text('title').notNull(),\n metadata: jsonb('metadata'), // Native JSONB\n isPublished: boolean('is_published').default(false),\n organizationId: text('organization_id').notNull(),\n});\n```\n\n### Key Differences\n\n| Feature | SQLite | PostgreSQL |\n|---------|--------|------------|\n| Boolean | `integer({ mode: 'boolean' })` | `boolean()` |\n| JSON | `text({ mode: 'json' })` | `jsonb()` or `json()` |\n| Auto-increment | `integer().primaryKey()` | `serial()` |\n| UUID | `text()` | `uuid()` |\n\n## Next Steps\n\n- [Configure the firewall](/compiler/definitions/firewall) for data isolation\n- [Set up access control](/compiler/definitions/access) for CRUD operations\n- [Define guards](/compiler/definitions/guards) for field modification rules\n- [Add custom actions](/compiler/definitions/actions) for business logic"
178
+ "content": "Quickback uses [Drizzle ORM](https://orm.drizzle.team/) to define your database schema. With `defineTable`, you combine your schema definition and security configuration in a single file.\n\n## Defining Tables with defineTable\n\nEach table gets its own file with schema and config together. Use the Drizzle dialect that matches your target database:\n\n| Target Database | Import From | Table Function |\n|-----------------|-------------|----------------|\n| Cloudflare D1, Turso, SQLite | `drizzle-orm/sqlite-core` | `sqliteTable` |\n| Supabase, Neon, PostgreSQL | `drizzle-orm/pg-core` | `pgTable` |\n| PlanetScale, MySQL | `drizzle-orm/mysql-core` | `mysqlTable` |\n\n```typescript\n// quickback/features/jobs/jobs.ts\n// For D1/SQLite targets:\n\nexport const jobs = sqliteTable('jobs', {\n id: text('id').primaryKey(),\n organizationId: text('organization_id').notNull(),\n title: text('title').notNull(),\n department: text('department').notNull(),\n status: text('status').notNull(), // \"draft\" | \"open\" | \"closed\"\n salaryMin: integer('salary_min'),\n salaryMax: integer('salary_max'),\n});\n\nexport default defineTable(jobs, {\n firewall: { organization: {} },\n guards: {\n createable: [\"title\", \"department\", \"status\", \"salaryMin\", \"salaryMax\"],\n updatable: [\"title\", \"department\", \"status\"],\n },\n crud: {\n list: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\", \"interviewer\"] } },\n get: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\", \"interviewer\"] } },\n create: { access: { roles: [\"owner\", \"hiring-manager\"] } },\n update: { access: { roles: [\"owner\", \"hiring-manager\"] } },\n delete: { access: { roles: [\"owner\", \"hiring-manager\"] } },\n },\n});\n\nexport type Job = typeof jobs.$inferSelect;\n```\n\n## Column Types\n\nDrizzle supports all standard SQL column types:\n\n| Type | Drizzle Function | Example |\n|------|------------------|---------|\n| String | `text()`, `varchar()` | `text('name')` |\n| Integer | `integer()`, `bigint()` | `integer('count')` |\n| Boolean | `boolean()` | `boolean('is_active')` |\n| Timestamp | `timestamp()` | `timestamp('created_at')` |\n| JSON | `json()`, `jsonb()` | `jsonb('metadata')` |\n| UUID | `uuid()` | `uuid('id')` |\n| Decimal | `decimal()`, `numeric()` | `decimal('price', { precision: 10, scale: 2 })` |\n\n## Column Modifiers\n\n```typescript\n// Required field\nname: text('name').notNull()\n\n// Default value\nisActive: boolean('is_active').default(true)\n\n// Primary key\nid: text('id').primaryKey()\n\n// Unique constraint\nemail: text('email').unique()\n\n// Default to current timestamp\ncreatedAt: timestamp('created_at').defaultNow()\n```\n\n## File Organization\n\nOrganize your tables by feature. Each feature directory contains table files:\n\n```\nquickback/\n├── quickback.config.ts\n└── features/\n ├── jobs/\n │ ├── jobs.ts # Main table + config\n │ ├── applications.ts # Related table + config\n │ └── actions.ts # Custom actions\n ├── candidates/\n │ ├── candidates.ts # Table + config\n │ └── candidate-notes.ts # Related table\n └── organizations/\n └── organizations.ts\n```\n\n**Key points:**\n- Tables with `export default defineTable(...)` get CRUD routes generated\n- Tables without a default export are internal (no routes, used for joins/relations)\n- Route paths are derived from filenames: `applications.ts` → `/api/v1/applications`\n\n### defineTable vs defineResource\n\nBoth functions are available:\n\n- **`defineTable`** - The standard function for defining tables with CRUD routes\n- **`defineResource`** - Alias for `defineTable`, useful when thinking in terms of REST resources\n\n```typescript\n// These are equivalent:\nexport default defineTable(jobs, { /* config */ });\nexport default defineResource(jobs, { /* config */ });\n```\n\n## 1 Resource = 1 Security Boundary\n\nEach `defineTable()` call defines a complete, self-contained security boundary. The security config you write — firewall, access, guards, and masking — is compiled into a single resource file that wraps all CRUD routes for that table.\n\nThis is a deliberate design choice. Mixing two resources with different security rules in one configuration would create ambiguity about which firewall, access, or masking rules apply to which table. By keeping it 1:1, there's never any question.\n\n| Scenario | What to do |\n|----------|------------|\n| Table needs its own API routes + security | Own file with `defineTable()` |\n| Table is internal/supporting (no direct API) | Extra `.ts` file in the parent feature directory, no `defineTable()` |\n\nA supporting table without `defineTable()` is useful when it's accessed internally — by action handlers, joins, or background jobs — but should never be directly exposed as its own API endpoint.\n\n## Internal Tables (No Routes)\n\nFor junction tables or internal data structures that shouldn't have API routes, simply omit the `defineTable` export:\n\n```typescript\n// quickback/features/jobs/interview-scores.ts\n\n// Internal scoring table - no routes needed\nexport const interviewScores = sqliteTable('interview_scores', {\n applicationId: text('application_id').notNull(),\n interviewerId: text('interviewer_id').notNull(),\n score: integer('score'),\n organizationId: text('organization_id').notNull(), // Always scope junction tables\n});\n\n// No default export = no CRUD routes generated\n```\n\nThese internal tables still participate in the database schema and migrations — they just don't get API routes or security configuration.\n\nThe compiler automatically injects **audit fields** (`createdAt`, `modifiedAt`, `deletedAt`, etc.) and **`organizationId`** into child/junction tables when the parent feature is org-scoped. This ensures cascade soft deletes and scoped queries work correctly without manual column definitions.\n\n## Audit Fields\n\nQuickback automatically adds and manages these audit fields on **all tables** in a feature (including child/junction tables without `defineTable`) - you don't need to define them:\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `createdAt` | timestamp | Set when record is created |\n| `createdBy` | text | User ID who created the record |\n| `modifiedAt` | timestamp | Updated on every change |\n| `modifiedBy` | text | User ID who last modified |\n| `deletedAt` | timestamp | Set on soft delete (optional) |\n| `deletedBy` | text | User ID who deleted (optional) |\n\nThe soft delete fields (`deletedAt`, `deletedBy`) are only added if your resource uses soft delete mode.\n\n### Disabling Audit Fields\n\nTo disable automatic audit fields for your project:\n\n```typescript\n// quickback.config.ts\nexport default defineConfig({\n // ...\n compiler: {\n features: {\n auditFields: false, // Disable for entire project\n }\n }\n});\n```\n\n### Protected System Fields\n\nThese fields are always protected and cannot be set by clients, even with `guards: false`:\n\n- `id` (when `generateId` is not `false`)\n- `createdAt`, `createdBy`\n- `modifiedAt`, `modifiedBy`\n- `deletedAt`, `deletedBy`\n\n## Ownership Fields\n\nFor the firewall to work, include the appropriate ownership columns:\n\n```typescript\n// For organization-scoped data (most common)\norganizationId: text('organization_id').notNull()\n\n// For user-owned data (personal data)\nownerId: text('owner_id').notNull()\n\n// For team-scoped data\nteamId: text('team_id').notNull()\n```\n\n## Display Column\n\nWhen a table has foreign key columns (e.g., `departmentId` referencing `departments`), Quickback can automatically resolve the human-readable label in GET and LIST responses. This eliminates the need for frontend lookup calls.\n\n### Auto-Detection\n\nQuickback auto-detects the display column by checking for common column names in this order:\n\n`name` → `title` → `label` → `headline` → `subject` → `code` → `displayName` → `fullName` → `description`\n\nIf your `departments` table has a `name` column, it's automatically used as the display column. No config needed.\n\n### Explicit Override\n\nOverride the auto-detected column with `displayColumn`:\n\n```typescript\nexport default defineTable(interviewStages, {\n displayColumn: 'label', // Use 'label' instead of auto-detected 'name'\n firewall: { organization: {} },\n crud: {\n list: { access: { roles: [\"hiring-manager\", \"recruiter\"] } },\n get: { access: { roles: [\"hiring-manager\", \"recruiter\"] } },\n },\n});\n```\n\n### How Labels Appear in Responses\n\nFor any FK column ending in `Id`, the API adds a `_label` field with the resolved display value:\n\n```json\n{\n \"id\": \"job_abc\",\n \"title\": \"Senior Engineer\",\n \"departmentId\": \"dept_xyz\",\n \"department_label\": \"Engineering\",\n \"locationId\": \"loc_123\",\n \"location_label\": \"San Francisco\"\n}\n```\n\nThe pattern is `{columnWithoutId}_label`. The frontend can find all labels with:\n\n```typescript\nObject.keys(record).filter(k => k.endsWith('_label'))\n```\n\nLabel resolution works within the same feature (tables in the same feature directory). System columns (`organizationId`, `createdBy`, `modifiedBy`) are never resolved.\n\nFor LIST endpoints, labels are batch-resolved efficiently — one query per FK column, not per record.\n\n## References\n\nWhen your FK columns don't match the target table name by convention (e.g., `vendorId` actually points to the `contact` table), declare explicit references so the CMS and schema registry know the correct target:\n\n```typescript\nexport default defineTable(applications, {\n references: {\n candidateId: \"candidate\",\n jobId: \"job\",\n referredById: \"candidate\",\n },\n // ... firewall, guards, crud\n});\n```\n\nEach key is a column name ending in `Id`, and the value is the camelCase table name it references. These mappings flow into the schema registry as `fkTarget` on each column, enabling the CMS to render typeahead/lookup inputs that search the correct table.\n\nConvention-based matching (strip `Id` suffix, look for a matching table) still works for simple cases like `projectId` → `project`. Use `references` only for columns where the convention doesn't match.\n\n## Input Hints\n\nControl how the CMS renders form inputs for specific columns. By default the CMS infers input types from the column's SQL type — `inputHints` lets you override that:\n\n```typescript\nexport default defineTable(jobs, {\n inputHints: {\n status: \"select\",\n department: \"select\",\n salaryMin: \"currency\",\n salaryMax: \"currency\",\n description: \"textarea\",\n },\n // ... firewall, guards, crud\n});\n```\n\n### Available Hint Values\n\n| Hint | Renders As |\n|------|-----------|\n| `select` | Dropdown select (single value) |\n| `multi-select` | Multi-value select |\n| `radio` | Radio button group |\n| `checkbox` | Checkbox toggle |\n| `textarea` | Multi-line text input |\n| `lookup` | FK typeahead search |\n| `hidden` | Hidden from forms |\n| `color` | Color picker |\n| `date` | Date picker |\n| `datetime` | Date + time picker |\n| `time` | Time picker |\n| `currency` | Currency input with formatting |\n\nInput hints are emitted in the schema registry as `inputHints` on the table metadata, and the CMS reads them to render the appropriate form controls.\n\n## Relations (Optional)\n\nDrizzle supports defining relations for type-safe joins:\n\n```typescript\n\nexport const jobsRelations = relations(jobs, ({ one, many }) => ({\n organization: one(organizations, {\n fields: [jobs.organizationId],\n references: [organizations.id],\n }),\n applications: many(applications),\n}));\n```\n\n## Database Configuration\n\nConfigure database options in your Quickback config:\n\n```typescript\n// quickback.config.ts\nexport default defineConfig({\n name: 'my-app',\n providers: {\n database: defineDatabase('cloudflare-d1', {\n generateId: 'prefixed', // 'uuid' | 'cuid' | 'nanoid' | 'prefixed' | 'serial' | false\n namingConvention: 'snake_case', // 'camelCase' | 'snake_case'\n usePlurals: false, // Auth table names: 'users' vs 'user'\n }),\n },\n compiler: {\n features: {\n auditFields: true, // Auto-manage audit timestamps\n }\n }\n});\n```\n\n## Choosing Your Dialect\n\nUse the Drizzle dialect that matches your database provider:\n\n### SQLite (D1, Turso, better-sqlite3)\n\n```typescript\n\nexport const jobs = sqliteTable('jobs', {\n id: text('id').primaryKey(),\n title: text('title').notNull(),\n metadata: text('metadata', { mode: 'json' }), // JSON stored as text\n isOpen: integer('is_open', { mode: 'boolean' }).default(false),\n organizationId: text('organization_id').notNull(),\n});\n```\n\n### PostgreSQL (Supabase, Neon)\n\n```typescript\n\nexport const jobs = pgTable('jobs', {\n id: serial('id').primaryKey(),\n title: text('title').notNull(),\n metadata: jsonb('metadata'), // Native JSONB\n isOpen: boolean('is_open').default(false),\n organizationId: text('organization_id').notNull(),\n});\n```\n\n### Key Differences\n\n| Feature | SQLite | PostgreSQL |\n|---------|--------|------------|\n| Boolean | `integer({ mode: 'boolean' })` | `boolean()` |\n| JSON | `text({ mode: 'json' })` | `jsonb()` or `json()` |\n| Auto-increment | `integer().primaryKey()` | `serial()` |\n| UUID | `text()` | `uuid()` |\n\n## Next Steps\n\n- [Configure the firewall](/compiler/definitions/firewall) for data isolation\n- [Set up access control](/compiler/definitions/access) for CRUD operations\n- [Define guards](/compiler/definitions/guards) for field modification rules\n- [Add custom actions](/compiler/definitions/actions) for business logic"
175
179
  },
176
180
  "compiler/definitions/validation": {
177
181
  "title": "Validation",
178
- "content": "## Current Validation: Action Input Schemas\n\nYou can validate input data in custom actions using Zod schemas:\n\n```typescript\n// features/invoices/actions.ts\n\nexport default defineActions(invoices, {\n approve: {\n description: \"Approve invoice for payment\",\n input: z.object({\n notes: z.string().min(1).max(500).optional(),\n amount: z.number().positive(),\n }),\n access: { roles: [\"admin\", \"finance\"] },\n execute: async ({ db, record, ctx, input }) => {\n // input is validated against the Zod schema before execution\n return record;\n },\n },\n});\n```\n\n## Planned: defineTable Validation\n\nIn a future release, `defineTable()` will support field-level validation rules enforced at the API level when creating or updating records.\n\n### Planned Validators\n\n| Validator | Description | Example |\n|-----------|-------------|---------|\n| `minLength` | Minimum string length | `minLength: 3` |\n| `maxLength` | Maximum string length | `maxLength: 255` |\n| `min` | Minimum number value | `min: 0` |\n| `max` | Maximum number value | `max: 100` |\n| `pattern` | Regex pattern match | `pattern: '^[a-z]+$'` |\n| `enum` | Allowed values list | `enum: ['draft', 'published']` |\n\n### Planned Usage\n\n```typescript\n// NOT YET AVAILABLE — planned syntax\nexport default defineTable(posts, {\n firewall: { organization: {} },\n guards: {\n createable: [\"title\", \"content\", \"status\"],\n updatable: [\"title\", \"content\", \"status\"],\n },\n validation: {\n title: { minLength: 1, maxLength: 255 },\n status: { enum: [\"draft\", \"published\", \"archived\"] },\n },\n crud: {\n list: { access: { roles: [\"member\"] } },\n create: { access: { roles: [\"member\"] } },\n update: { access: { roles: [\"member\"] } },\n },\n});\n```"
182
+ "content": "## Action Input Validation\n\nYou can validate input data in custom actions using Zod schemas. The input is validated before the action handler executes:\n\n```typescript\n// quickback/features/applications/actions.ts\n\nexport default defineActions(applications, {\n 'advance-stage': {\n description: \"Move application to the next pipeline stage\",\n input: z.object({\n stage: z.enum([\"screening\", \"interview\", \"offer\", \"hired\"]),\n notes: z.string().min(1).max(500).optional(),\n }),\n access: { roles: [\"owner\", \"hiring-manager\"] },\n execute: async ({ db, record, ctx, input }) => {\n // input is validated against the Zod schema before execution\n return record;\n },\n },\n});\n```\n\nIf validation fails, the API returns a `400 Bad Request` with details about which fields failed.\n\nSee [Actions](/compiler/definitions/actions) for the full action definition reference."
179
183
  },
180
184
  "compiler/definitions/views": {
181
185
  "title": "Views - Column Level Security",
182
- "content": "Views implement **Column Level Security (CLS)** - controlling which columns users can access. This complements **Row Level Security (RLS)** provided by the [Firewall](/compiler/definitions/firewall), which controls which rows users can access.\n\n| Security Layer | Controls | Quickback Feature |\n|----------------|----------|-------------------|\n| Row Level Security | Which records | Firewall |\n| Column Level Security | Which fields | Views |\n\nViews provide named field projections with role-based access control. Use views to return different sets of columns to different users without duplicating CRUD endpoints.\n\n## When to Use Views\n\n| Concept | Purpose | Example |\n|---------|---------|---------|\n| CRUD list | Full records | Returns all columns |\n| Masking | Hide values | SSN `*****6789` |\n| Views | Exclude columns | Only `id`, `name`, `status` |\n\n- **CRUD list** returns all fields to authorized users\n- **Masking** transforms sensitive values but still includes the column\n- **Views** completely exclude columns from the response\n\n## Basic Usage\n\n```typescript\n// features/customers/customers.ts\n\nexport const customers = sqliteTable('customers', {\n id: text('id').primaryKey(),\n name: text('name').notNull(),\n email: text('email').notNull(),\n phone: text('phone'),\n ssn: text('ssn'),\n internalNotes: text('internal_notes'),\n organizationId: text('organization_id').notNull(),\n});\n\nexport default defineTable(customers, {\n masking: {\n ssn: { type: 'ssn', show: { roles: ['admin'] } },\n },\n\n crud: {\n list: { access: { roles: ['owner', 'admin', 'member'] } },\n get: { access: { roles: ['owner', 'admin', 'member'] } },\n },\n\n // Views: Named field projections\n views: {\n summary: {\n fields: ['id', 'name', 'email'],\n access: { roles: ['owner', 'admin', 'member'] },\n },\n full: {\n fields: ['id', 'name', 'email', 'phone', 'ssn', 'internalNotes'],\n access: { roles: ['admin'] },\n },\n report: {\n fields: ['id', 'name', 'email', 'createdAt'],\n access: { roles: ['finance', 'admin'] },\n },\n },\n});\n```\n\n## Generated Endpoints\n\nViews generate GET endpoints at `/{resource}/views/{viewName}`:\n\n```\nGET /api/v1/customers # CRUD list - all fields\nGET /api/v1/customers/views/summary # View - only summary fields\nGET /api/v1/customers/views/full # View - all fields (admin only)\nGET /api/v1/customers/views/report # View - report fields\n```\n\n## Query Parameters\n\nViews support the same query parameters as the list endpoint:\n\n### Pagination\n\n| Parameter | Description | Default | Max |\n|-----------|-------------|---------|-----|\n| `limit` | Number of records to return | 50 | 100 |\n| `offset` | Number of records to skip | 0 | - |\n\n```bash\n# Get first 10 records\nGET /api/v1/customers/views/summary?limit=10\n\n# Get records 11-20\nGET /api/v1/customers/views/summary?limit=10&offset=10\n```\n\n### Filtering\n\n```bash\n# Filter by exact value\nGET /api/v1/customers/views/summary?status=active\n\n# Filter with operators\nGET /api/v1/customers/views/summary?createdAt.gt=2024-01-01\n\n# Multiple filters (AND logic)\nGET /api/v1/customers/views/summary?status=active&email.like=yourdomain.com\n```\n\n### Sorting\n\n| Parameter | Description | Default |\n|-----------|-------------|---------|\n| `sort` | Field to sort by | `createdAt` |\n| `order` | Sort direction (`asc` or `desc`) | `desc` |\n\n```bash\nGET /api/v1/customers/views/summary?sort=name&order=asc\n```\n\n## Security\n\nAll four security pillars apply to views:\n\n| Pillar | Behavior |\n|--------|----------|\n| **Firewall** | WHERE clause applied (same as list) |\n| **Access** | Per-view access control |\n| **Guards** | N/A (read-only) |\n| **Masking** | Applied to returned fields |\n\n### Firewall\n\nViews automatically apply the same firewall conditions as the list endpoint. Users only see records within their organization scope.\n\n### Access Control\n\nEach view has its own access configuration:\n\n```typescript\nviews: {\n // Public view - available to all members\n summary: {\n fields: ['id', 'name', 'status'],\n access: { roles: ['owner', 'admin', 'member'] },\n },\n // Restricted view - admin only\n full: {\n fields: ['id', 'name', 'status', 'ssn', 'internalNotes'],\n access: { roles: ['admin'] },\n },\n}\n```\n\n### Masking\n\nMasking rules are applied to the returned fields. If a view includes a masked field like `ssn`, the masking rules still apply:\n\n```typescript\nmasking: {\n ssn: { type: 'ssn', show: { roles: ['admin'] } },\n},\n\nviews: {\n // Even if a member accesses the 'full' view, ssn will be masked\n // because masking rules take precedence\n full: {\n fields: ['id', 'name', 'ssn'],\n access: { roles: ['owner', 'admin', 'member'] },\n },\n}\n```\n\n### How Views and Masking Work Together\n\nViews and masking are orthogonal concerns:\n- **Views** control field selection (which columns appear)\n- **Masking** controls field transformation (how values appear based on role)\n\nExample configuration:\n\n```typescript\nmasking: {\n ssn: { type: 'ssn', show: { roles: ['admin'] } },\n},\n\nviews: {\n summary: {\n fields: ['id', 'name'], // ssn NOT included\n access: { roles: ['owner', 'admin', 'member'] },\n },\n full: {\n fields: ['id', 'name', 'ssn'], // ssn included\n access: { roles: ['owner', 'admin', 'member'] },\n },\n}\n```\n\n| Endpoint | Role | `ssn` in response? | `ssn` value |\n|----------|------|-------------------|-------------|\n| `/views/summary` | member | No | N/A |\n| `/views/summary` | admin | No | N/A |\n| `/views/full` | member | Yes | `*****6789` |\n| `/views/full` | admin | Yes | `123-45-6789` |\n\n## Response Format\n\nView responses include metadata about the view:\n\n```json\n{\n \"data\": [\n { \"id\": \"cust_123\", \"name\": \"John Doe\", \"email\": \"john@yourdomain.com\" },\n { \"id\": \"cust_456\", \"name\": \"Jane Smith\", \"email\": \"jane@yourdomain.com\" }\n ],\n \"view\": \"summary\",\n \"fields\": [\"id\", \"name\", \"email\"],\n \"pagination\": {\n \"limit\": 50,\n \"offset\": 0,\n \"count\": 2\n }\n}\n```\n\n## Complete Example\n\n```typescript\n// features/employees/employees.ts\nexport default defineTable(employees, {\n guards: {\n createable: ['name', 'email', 'phone', 'department'],\n updatable: ['name', 'email', 'phone'],\n },\n\n masking: {\n ssn: { type: 'ssn', show: { roles: ['hr', 'admin'] } },\n salary: { type: 'redact', show: { roles: ['hr', 'admin'] } },\n personalEmail: { type: 'email', show: { or: 'owner' } },\n },\n\n crud: {\n list: { access: { roles: ['owner', 'admin', 'member'] } },\n get: { access: { roles: ['owner', 'admin', 'member'] } },\n create: { access: { roles: ['owner', 'admin'] } },\n update: { access: { roles: ['owner', 'admin'] } },\n delete: { access: { roles: ['owner', 'admin'] } },\n },\n\n views: {\n // Directory view - public employee info\n directory: {\n fields: ['id', 'name', 'email', 'department'],\n access: { roles: ['owner', 'admin', 'member'] },\n },\n // HR view - includes sensitive info\n hr: {\n fields: ['id', 'name', 'email', 'phone', 'ssn', 'salary', 'department', 'startDate'],\n access: { roles: ['owner', 'admin'] },\n },\n // Payroll export\n payroll: {\n fields: ['id', 'name', 'ssn', 'salary', 'bankAccount'],\n access: { roles: ['owner', 'admin'] },\n },\n },\n});\n```"
186
+ "content": "Views implement **Column Level Security (CLS)** - controlling which columns users can access. This complements **Row Level Security (RLS)** provided by the [Firewall](/compiler/definitions/firewall), which controls which rows users can access.\n\n| Security Layer | Controls | Quickback Feature |\n|----------------|----------|-------------------|\n| Row Level Security | Which records | Firewall |\n| Column Level Security | Which fields | Views |\n\nViews provide named field projections with role-based access control. Use views to return different sets of columns to different users without duplicating CRUD endpoints.\n\n## When to Use Views\n\n| Concept | Purpose | Example |\n|---------|---------|---------|\n| CRUD list | Full records | Returns all columns |\n| Masking | Hide values | Email `j***@c******.com` |\n| Views | Exclude columns | Only `id`, `name`, `source` |\n\n- **CRUD list** returns all fields to authorized users\n- **Masking** transforms sensitive values but still includes the column\n- **Views** completely exclude columns from the response\n\n## Basic Usage\n\n```typescript\n// features/candidates/candidates.ts\n\nexport const candidates = sqliteTable('candidates', {\n id: text('id').primaryKey(),\n name: text('name').notNull(),\n email: text('email').notNull(),\n phone: text('phone'),\n source: text('source'),\n resumeUrl: text('resume_url'),\n internalNotes: text('internal_notes'),\n organizationId: text('organization_id').notNull(),\n});\n\nexport default defineTable(candidates, {\n masking: {\n email: { type: 'email', show: { roles: ['hiring-manager', 'recruiter'] } },\n phone: { type: 'phone', show: { roles: ['hiring-manager', 'recruiter'] } },\n },\n\n crud: {\n list: { access: { roles: ['owner', 'hiring-manager', 'recruiter', 'interviewer'] } },\n get: { access: { roles: ['owner', 'hiring-manager', 'recruiter', 'interviewer'] } },\n },\n\n // Views: Named field projections\n views: {\n pipeline: {\n fields: ['id', 'name', 'source'],\n access: { roles: ['owner', 'hiring-manager', 'recruiter', 'interviewer'] },\n },\n full: {\n fields: ['id', 'name', 'email', 'phone', 'source', 'resumeUrl', 'internalNotes'],\n access: { roles: ['hiring-manager', 'recruiter'] },\n },\n report: {\n fields: ['id', 'name', 'source', 'createdAt'],\n access: { roles: ['owner', 'hiring-manager'] },\n },\n },\n});\n```\n\n## Generated Endpoints\n\nViews generate GET endpoints at `/{resource}/views/{viewName}`:\n\n```\nGET /api/v1/candidates # CRUD list - all fields\nGET /api/v1/candidates/views/pipeline # View - only pipeline fields\nGET /api/v1/candidates/views/full # View - all fields (hiring-manager/recruiter only)\nGET /api/v1/candidates/views/report # View - report fields\n```\n\n## Query Parameters\n\nViews support the same query parameters as the list endpoint:\n\n### Pagination\n\n| Parameter | Description | Default | Max |\n|-----------|-------------|---------|-----|\n| `limit` | Number of records to return | 50 | 100 |\n| `offset` | Number of records to skip | 0 | - |\n\n```bash\n# Get first 10 records\nGET /api/v1/candidates/views/pipeline?limit=10\n\n# Get records 11-20\nGET /api/v1/candidates/views/pipeline?limit=10&offset=10\n```\n\n### Filtering\n\n```bash\n# Filter by exact value\nGET /api/v1/candidates/views/pipeline?source=linkedin\n\n# Filter with operators\nGET /api/v1/candidates/views/pipeline?createdAt.gt=2024-01-01\n\n# Multiple filters (AND logic)\nGET /api/v1/candidates/views/pipeline?source=linkedin&name.like=Smith\n```\n\n### Sorting\n\n| Parameter | Description | Default |\n|-----------|-------------|---------|\n| `sort` | Field to sort by | `createdAt` |\n| `order` | Sort direction (`asc` or `desc`) | `desc` |\n\n```bash\nGET /api/v1/candidates/views/pipeline?sort=name&order=asc\n```\n\n## Security\n\nAll four security pillars apply to views:\n\n| Pillar | Behavior |\n|--------|----------|\n| **Firewall** | WHERE clause applied (same as list) |\n| **Access** | Per-view access control |\n| **Guards** | N/A (read-only) |\n| **Masking** | Applied to returned fields |\n\n### Firewall\n\nViews automatically apply the same firewall conditions as the list endpoint. Users only see records within their organization scope.\n\n### Access Control\n\nEach view has its own access configuration:\n\n```typescript\nviews: {\n // Pipeline view - available to all roles\n pipeline: {\n fields: ['id', 'name', 'source'],\n access: { roles: ['owner', 'hiring-manager', 'recruiter', 'interviewer'] },\n },\n // Full view - recruiter and above\n full: {\n fields: ['id', 'name', 'source', 'email', 'phone', 'internalNotes'],\n access: { roles: ['hiring-manager', 'recruiter'] },\n },\n}\n```\n\n### Masking\n\nMasking rules are applied to the returned fields. If a view includes a masked field like `email`, the masking rules still apply:\n\n```typescript\nmasking: {\n email: { type: 'email', show: { roles: ['hiring-manager', 'recruiter'] } },\n},\n\nviews: {\n // Even if an interviewer accesses the 'full' view, email will be masked\n // because masking rules take precedence\n full: {\n fields: ['id', 'name', 'email'],\n access: { roles: ['owner', 'hiring-manager', 'recruiter', 'interviewer'] },\n },\n}\n```\n\n### How Views and Masking Work Together\n\nViews and masking are orthogonal concerns:\n- **Views** control field selection (which columns appear)\n- **Masking** controls field transformation (how values appear based on role)\n\nExample configuration:\n\n```typescript\nmasking: {\n email: { type: 'email', show: { roles: ['hiring-manager', 'recruiter'] } },\n},\n\nviews: {\n pipeline: {\n fields: ['id', 'name'], // email NOT included\n access: { roles: ['owner', 'hiring-manager', 'recruiter', 'interviewer'] },\n },\n full: {\n fields: ['id', 'name', 'email'], // email included\n access: { roles: ['owner', 'hiring-manager', 'recruiter', 'interviewer'] },\n },\n}\n```\n\n| Endpoint | Role | `email` in response? | `email` value |\n|----------|------|---------------------|---------------|\n| `/views/pipeline` | interviewer | No | N/A |\n| `/views/pipeline` | recruiter | No | N/A |\n| `/views/full` | interviewer | Yes | `j***@c******.com` |\n| `/views/full` | recruiter | Yes | `jane@company.com` |\n\n## Response Format\n\nView responses include metadata about the view:\n\n```json\n{\n \"data\": [\n { \"id\": \"cnd_123\", \"name\": \"Jane Doe\", \"source\": \"linkedin\" },\n { \"id\": \"cnd_456\", \"name\": \"John Smith\", \"source\": \"referral\" }\n ],\n \"view\": \"pipeline\",\n \"fields\": [\"id\", \"name\", \"source\"],\n \"pagination\": {\n \"limit\": 50,\n \"offset\": 0,\n \"count\": 2\n }\n}\n```\n\n## Complete Example\n\n```typescript\n// features/jobs/jobs.ts\nexport default defineTable(jobs, {\n guards: {\n createable: ['title', 'department', 'status', 'salaryMin', 'salaryMax'],\n updatable: ['title', 'department', 'status'],\n },\n\n masking: {\n salaryMin: { type: 'redact', show: { roles: ['owner', 'hiring-manager'] } },\n salaryMax: { type: 'redact', show: { roles: ['owner', 'hiring-manager'] } },\n },\n\n crud: {\n list: { access: { roles: ['owner', 'hiring-manager', 'recruiter', 'interviewer'] } },\n get: { access: { roles: ['owner', 'hiring-manager', 'recruiter', 'interviewer'] } },\n create: { access: { roles: ['owner', 'hiring-manager'] } },\n update: { access: { roles: ['owner', 'hiring-manager'] } },\n delete: { access: { roles: ['owner', 'hiring-manager'] } },\n },\n\n views: {\n // Public job board view\n board: {\n fields: ['id', 'title', 'department', 'status'],\n access: { roles: ['owner', 'hiring-manager', 'recruiter', 'interviewer'] },\n },\n // Internal view with salary info\n internal: {\n fields: ['id', 'title', 'department', 'status', 'salaryMin', 'salaryMax'],\n access: { roles: ['owner', 'hiring-manager'] },\n },\n // Compensation report\n compensation: {\n fields: ['id', 'title', 'department', 'salaryMin', 'salaryMax'],\n access: { roles: ['owner', 'hiring-manager'] },\n },\n },\n});\n```"
183
187
  },
184
188
  "compiler/getting-started/claude-code": {
185
189
  "title": "Claude Code Skill",
186
- "content": "The Quickback CLI includes a skill for [Claude Code](https://claude.com/claude-code) that gives Claude context about your project structure, definitions, and the Quickback compiler. This helps Claude write correct `defineTable()` definitions, actions, and configuration.\n\n## Installation\n\n### Install the Skill\n\n```bash\nquickback claude install\n```\n\nThis installs the Quickback skill files into your project so Claude Code can use them.\n\n**Options:**\n\n| Flag | Description |\n|------|-------------|\n| `--global` | Install to `~/.claude/` (available in all projects) |\n| `--local` | Install to `./quickback/.claude/` (project-specific, default) |\n\nAfter installing, start Claude Code in your project directory. The Quickback skill will be available automatically.\n\n## What the Skill Provides\n\nThe Quickback skill teaches Claude Code about:\n\n- **Project structure** — Where definitions, features, and config files live\n- **`defineTable()` API** — How to write schema + security definitions correctly\n- **`defineActions()` API** — How to write custom actions with input validation\n- **Security pillars** — Firewall, Access, Guards, and Masking configuration\n- **Provider options** — Cloudflare, Bun, Turso runtime and database choices\n- **Compiler workflow** — How to compile and deploy\n\n## Usage Examples\n\nWith the skill installed, you can ask Claude Code to:\n\n```\n\"Add a customers feature with org-scoped firewall and email masking\"\n\n\"Create an action on orders called 'refund' that requires admin role\"\n\n\"Add a summary view to the projects feature showing only id, name, and status\"\n\n\"Switch my config from Bun to Cloudflare for production deployment\"\n```\n\nClaude will generate correct Quickback definitions that you can compile directly.\n\n## Updating\n\nTo update the skill to the latest version:\n\n```bash\nquickback claude install\n```\n\nThis overwrites the existing skill files with the latest version from the CLI.\n\n## See Also\n\n- [Claude Code Plugin](/plugins-tools/claude-code-skill) — Full plugin reference\n- [Getting Started](/compiler/getting-started) — Project setup guide"
190
+ "content": "The Quickback CLI includes a skill for [Claude Code](https://claude.com/claude-code) that gives Claude context about your project structure, definitions, and the Quickback compiler. This helps Claude write correct `defineTable()` definitions, actions, and configuration.\n\n## Installation\n\n### Install the Skill\n\n```bash\nquickback claude install\n```\n\nThis installs the Quickback skill files into your project so Claude Code can use them.\n\n**Options:**\n\n| Flag | Description |\n|------|-------------|\n| `--global` | Install to `~/.claude/` (available in all projects) |\n| `--local` | Install to `./quickback/.claude/` (project-specific, default) |\n\nAfter installing, start Claude Code in your project directory. The Quickback skill will be available automatically.\n\n## What the Skill Provides\n\nThe Quickback skill teaches Claude Code about:\n\n- **Project structure** — Where definitions, features, and config files live\n- **`defineTable()` API** — How to write schema + security definitions correctly\n- **`defineActions()` API** — How to write custom actions with input validation\n- **Security pillars** — Firewall, Access, Guards, and Masking configuration\n- **Provider options** — Cloudflare, Bun, Turso runtime and database choices\n- **Compiler workflow** — How to compile and deploy\n\n## Usage Examples\n\nWith the skill installed, you can ask Claude Code to:\n\n```\n\"Add a candidates feature with org-scoped firewall and email/phone masking for interviewers\"\n\n\"Create an action on applications called 'advance-stage' that requires hiring-manager role\"\n\n\"Add a pipeline view to the candidates feature showing only id, name, and source\"\n\n\"Switch my config from Bun to Cloudflare for production deployment\"\n```\n\nClaude will generate correct Quickback definitions that you can compile directly.\n\n## Updating\n\nTo update the skill to the latest version:\n\n```bash\nquickback claude install\n```\n\nThis overwrites the existing skill files with the latest version from the CLI.\n\n## See Also\n\n- [Claude Code Plugin](/plugins-tools/claude-code-skill) — Full plugin reference\n- [Getting Started](/compiler/getting-started) — Project setup guide"
187
191
  },
188
192
  "compiler/getting-started/full-example": {
189
193
  "title": "Complete Example",
190
- "content": "This example shows a complete resource definition using all five security layers, and the production code Quickback generates from it.\n\n## What You Define\n\nA `customers` resource for a multi-tenant SaaS application with:\n- Organization-level data isolation\n- Role-based CRUD permissions\n- Field-level write protection\n- PII masking for sensitive fields\n- Column-level views for different access levels\n\n### Schema\n\n```typescript title=\"definitions/features/customers/schema.ts\"\n\nexport const customers = sqliteTable('customers', {\n id: text('id').primaryKey(),\n organizationId: text('organization_id').notNull(),\n name: text('name').notNull(),\n email: text('email').notNull(),\n phone: text('phone'),\n ssn: text('ssn'),\n});\n\n// Note: createdAt, createdBy, modifiedAt, modifiedBy are auto-injected\n```\n\n### Resource Configuration\n\n```typescript title=\"definitions/features/customers/resource.ts\"\n\nexport default defineResource(customers, {\n // FIREWALL: Data isolation\n firewall: {\n organization: {}\n },\n\n // ACCESS: Role-based permissions\n crud: {\n list: { access: { roles: ['owner', 'admin', 'member', 'support'] } },\n get: { access: { roles: ['owner', 'admin', 'member', 'support'] } },\n create: { access: { roles: ['admin'] } },\n update: { access: { roles: ['admin', 'support'] } },\n delete: { access: { roles: ['admin'] }, mode: 'hard' },\n },\n\n // GUARDS: Field-level protection\n guards: {\n createable: ['name', 'email', 'phone', 'ssn'],\n updatable: ['name', 'email', 'phone'],\n immutable: ['ssn'], // Can set once, never change\n },\n\n // MASKING: PII redaction\n masking: {\n ssn: { type: 'ssn', show: { roles: ['admin'] } },\n phone: { type: 'phone', show: { roles: ['admin', 'support'] } },\n email: { type: 'email', show: { roles: ['admin'] } },\n },\n\n // VIEWS: Column-level projections\n views: {\n summary: {\n fields: ['id', 'name', 'email'],\n access: { roles: ['owner', 'admin', 'member', 'support'] },\n },\n full: {\n fields: ['id', 'name', 'email', 'phone', 'ssn'],\n access: { roles: ['owner', 'admin'] },\n },\n },\n});\n```\n\n---\n\n## What Quickback Generates\n\nRun `quickback compile` and Quickback generates production-ready code.\n\n### Security Helpers\n\n```typescript title=\"src/features/customers/customers.resource.ts\"\n\n/**\n * Firewall conditions for customers\n * Pattern: Organization only\n */\nexport function buildFirewallConditions(ctx: AppContext) {\n const conditions = [];\n\n // Organization isolation\n conditions.push(eq(customers.organizationId, ctx.activeOrgId!));\n\n return and(...conditions);\n}\n\n/**\n * Guards configuration for field modification rules\n */\nexport const GUARDS_CONFIG = {\n createable: new Set(['name', 'email', 'phone', 'ssn']),\n updatable: new Set(['name', 'email', 'phone']),\n immutable: new Set(['ssn']),\n protected: {},\n systemManaged: new Set([\n 'createdAt', 'createdBy',\n 'modifiedAt', 'modifiedBy',\n 'deletedAt', 'deletedBy'\n ]),\n};\n\n/**\n * Validates field input for CREATE operations.\n */\nexport function validateCreate(input: Record<string, any>) {\n const systemManaged: string[] = [];\n const protectedFields: Array<{ field: string; actions: string[] }> = [];\n const notCreateable: string[] = [];\n\n for (const field of Object.keys(input)) {\n if (GUARDS_CONFIG.systemManaged.has(field)) {\n systemManaged.push(field);\n continue;\n }\n if (field in GUARDS_CONFIG.protected) {\n const actions = [...GUARDS_CONFIG.protected[field]];\n protectedFields.push({ field, actions });\n continue;\n }\n if (!GUARDS_CONFIG.createable.has(field) && !GUARDS_CONFIG.immutable.has(field)) {\n notCreateable.push(field);\n }\n }\n\n const valid = systemManaged.length === 0 &&\n protectedFields.length === 0 &&\n notCreateable.length === 0;\n\n return { valid, systemManaged, protected: protectedFields, notCreateable };\n}\n\n/**\n * Validates field input for UPDATE operations.\n */\nexport function validateUpdate(input: Record<string, any>) {\n const systemManaged: string[] = [];\n const immutable: string[] = [];\n const protectedFields: Array<{ field: string; actions: string[] }> = [];\n const notUpdatable: string[] = [];\n\n for (const field of Object.keys(input)) {\n if (GUARDS_CONFIG.systemManaged.has(field)) {\n systemManaged.push(field);\n continue;\n }\n if (GUARDS_CONFIG.immutable.has(field)) {\n immutable.push(field);\n continue;\n }\n if (field in GUARDS_CONFIG.protected) {\n const actions = [...GUARDS_CONFIG.protected[field]];\n protectedFields.push({ field, actions });\n continue;\n }\n if (!GUARDS_CONFIG.updatable.has(field)) {\n notUpdatable.push(field);\n }\n }\n\n const valid = systemManaged.length === 0 &&\n immutable.length === 0 &&\n protectedFields.length === 0 &&\n notUpdatable.length === 0;\n\n return { valid, systemManaged, immutable, protected: protectedFields, notUpdatable };\n}\n\n/**\n * Masks sensitive fields based on user role\n */\nexport function maskCustomer<T extends Record<string, any>>(\n record: T,\n ctx: AppContext\n): T {\n const masked: any = { ...record };\n\n // email: show to admin only\n if (!ctx.roles?.includes('admin')) {\n if (masked['email'] != null) {\n masked['email'] = masks.email(masked['email']);\n }\n }\n\n // phone: show to admin, support\n if (!ctx.roles?.some(r => ['admin', 'support'].includes(r))) {\n if (masked['phone'] != null) {\n masked['phone'] = masks.phone(masked['phone']);\n }\n }\n\n // ssn: show to admin only\n if (!ctx.roles?.includes('admin')) {\n if (masked['ssn'] != null) {\n masked['ssn'] = masks.ssn(masked['ssn']);\n }\n }\n\n return masked as T;\n}\n\nexport function maskCustomers<T extends Record<string, any>>(\n records: T[],\n ctx: AppContext\n): T[] {\n return records.map(r => maskCustomer(r, ctx));\n}\n\n// CRUD Access configuration\nexport const CRUD_ACCESS = {\n list: { access: { roles: ['owner', 'admin', 'member', 'support'] } },\n get: { access: { roles: ['owner', 'admin', 'member', 'support'] } },\n create: { access: { roles: ['admin'] } },\n update: { access: { roles: ['admin', 'support'] } },\n delete: { access: { roles: ['admin'] }, mode: 'hard' },\n};\n\n// Views configuration\nexport const VIEWS_CONFIG = {\n summary: {\n fields: ['id', 'name', 'email'],\n access: { roles: ['owner', 'admin', 'member', 'support'] },\n },\n full: {\n fields: ['id', 'name', 'email', 'phone', 'ssn'],\n access: { roles: ['owner', 'admin'] },\n },\n};\n```\n\n### API Routes\n\nThe generated routes wire everything together:\n\n```typescript title=\"src/features/customers/customers.routes.ts\"\n\n buildFirewallConditions,\n validateCreate,\n validateUpdate,\n maskCustomer,\n maskCustomers,\n CRUD_ACCESS\n} from './customers.resource';\n\nconst app = new Hono();\n\n// GET /customers - List with firewall + masking\napp.get('/', async (c) => {\n const ctx = c.get('ctx');\n const db = c.get('db');\n\n // Access check\n if (!await evaluateAccess(CRUD_ACCESS.list.access, ctx)) {\n return c.json(AccessErrors.roleRequired(\n CRUD_ACCESS.list.access.roles,\n ctx.roles\n ), 403);\n }\n\n // Query with firewall conditions\n const results = await db.select().from(customers)\n .where(buildFirewallConditions(ctx));\n\n // Apply masking before returning\n return c.json({\n data: maskCustomers(results, ctx),\n });\n});\n\n// POST /customers - Create with guards\napp.post('/', async (c) => {\n const ctx = c.get('ctx');\n const db = c.get('db');\n\n // Access check\n if (!await evaluateAccess(CRUD_ACCESS.create.access, ctx)) {\n return c.json(AccessErrors.roleRequired(\n CRUD_ACCESS.create.access.roles,\n ctx.roles\n ), 403);\n }\n\n const body = await c.req.json();\n\n // Guards validation\n const validation = validateCreate(body);\n if (!validation.valid) {\n if (validation.systemManaged.length > 0) {\n return c.json(GuardErrors.systemManaged(validation.systemManaged), 400);\n }\n if (validation.notCreateable.length > 0) {\n return c.json(GuardErrors.fieldNotCreateable(validation.notCreateable), 400);\n }\n }\n\n // Apply ownership and audit fields\n const data = {\n id: 'cst_' + crypto.randomUUID().replace(/-/g, ''),\n ...body,\n organizationId: ctx.activeOrgId,\n createdAt: new Date().toISOString(),\n createdBy: ctx.userId,\n modifiedAt: new Date().toISOString(),\n modifiedBy: ctx.userId,\n };\n\n const result = await db.insert(customers).values(data).returning();\n return c.json(maskCustomer(result[0], ctx), 201);\n});\n\n// PATCH /customers/:id - Update with guards\napp.patch('/:id', async (c) => {\n const ctx = c.get('ctx');\n const db = c.get('db');\n const id = c.req.param('id');\n\n // Fetch with firewall\n const [record] = await db.select().from(customers)\n .where(and(buildFirewallConditions(ctx), eq(customers.id, id)));\n\n if (!record) {\n return c.json({ error: 'Not found', code: 'NOT_FOUND' }, 404);\n }\n\n // Access check\n if (!await evaluateAccess(CRUD_ACCESS.update.access, ctx)) {\n return c.json(AccessErrors.roleRequired(\n CRUD_ACCESS.update.access.roles,\n ctx.roles\n ), 403);\n }\n\n const body = await c.req.json();\n\n // Guards validation\n const validation = validateUpdate(body);\n if (!validation.valid) {\n if (validation.immutable.length > 0) {\n return c.json(GuardErrors.fieldImmutable(validation.immutable), 400);\n }\n if (validation.notUpdatable.length > 0) {\n return c.json(GuardErrors.fieldNotUpdatable(validation.notUpdatable), 400);\n }\n }\n\n // Apply audit fields\n const data = {\n ...body,\n modifiedAt: new Date().toISOString(),\n modifiedBy: ctx.userId,\n };\n\n const result = await db.update(customers).set(data)\n .where(eq(customers.id, id)).returning();\n\n return c.json(maskCustomer(result[0], ctx));\n});\n\nexport default app;\n```\n\n---\n\n## Runtime Behavior\n\n### Masking by Role\n\n| Role | `email` | `phone` | `ssn` |\n|------|---------|---------|-------|\n| **admin** | `john@example.com` | `555-123-4567` | `123-45-6789` |\n| **support** | `j***@e******.com` | `555-123-4567` | `*****6789` |\n| **member** | `j***@e******.com` | `******4567` | `*****6789` |\n\n### Views + Masking Interaction\n\nViews control **which fields are returned**. Masking controls **what values are shown**.\n\n| Endpoint | Role | `ssn` in response? | `ssn` value |\n|----------|------|--------------------|-------------|\n| `/customers/views/summary` | member | No | N/A |\n| `/customers/views/summary` | admin | No | N/A |\n| `/customers/views/full` | member | Yes | `*****6789` |\n| `/customers/views/full` | admin | Yes | `123-45-6789` |\n\n### Guards Enforcement\n\n```bash\n# ✅ Allowed: Create with createable fields\nPOST /customers\n{ \"name\": \"Jane\", \"email\": \"jane@example.com\", \"ssn\": \"987-65-4321\" }\n\n# ❌ Rejected: Update immutable field\nPATCH /customers/cst_123\n{ \"ssn\": \"000-00-0000\" }\n# → 400: \"Field 'ssn' is immutable and cannot be modified\"\n\n# ❌ Rejected: Set system-managed field\nPOST /customers\n{ \"name\": \"Jane\", \"createdBy\": \"hacker\" }\n# → 400: \"System-managed fields cannot be set: createdBy\"\n```\n\n---\n\n## Generated API Endpoints\n\n| Method | Endpoint | Access |\n|--------|----------|--------|\n| `GET` | `/customers` | owner, admin, member, support |\n| `GET` | `/customers/:id` | owner, admin, member, support |\n| `POST` | `/customers` | admin |\n| `PATCH` | `/customers/:id` | admin, support |\n| `DELETE` | `/customers/:id` | admin |\n| `GET` | `/customers/views/summary` | owner, admin, member, support |\n| `GET` | `/customers/views/full` | admin |\n\n---\n\n## Key Takeaways\n\n1. **~50 lines of configuration** generates **~500+ lines of production code**\n2. **Security is declarative** - you describe *what* you want, not *how*\n3. **All layers work together** - Firewall → Access → Guards → Masking\n4. **Code is readable and auditable** - no magic, just TypeScript\n5. **Type-safe from definition to API** - Drizzle schema drives everything"
194
+ "content": "This example shows a complete resource definition using all five security layers, and the production code Quickback generates from it.\n\n## What You Define\n\nA `candidates` resource for a multi-tenant recruitment application with:\n- Organization-level data isolation\n- Role-based CRUD permissions\n- Field-level write protection\n- PII masking for sensitive fields\n- Column-level views for different access levels\n\n```typescript title=\"quickback/features/candidates/candidates.ts\"\n\n// Schema\nexport const candidates = sqliteTable('candidates', {\n id: text('id').primaryKey(),\n organizationId: text('organization_id').notNull(),\n name: text('name').notNull(),\n email: text('email').notNull(),\n phone: text('phone'),\n resumeUrl: text('resume_url'),\n source: text('source'), // e.g. \"linkedin\", \"referral\", \"careers-page\"\n internalNotes: text('internal_notes'),\n // Note: createdAt, createdBy, modifiedAt, modifiedBy are auto-injected\n});\n\n// Security Configuration\nexport default defineTable(candidates, {\n // FIREWALL: Data isolation\n firewall: {\n organization: {}\n },\n\n // ACCESS: Role-based permissions\n crud: {\n list: { access: { roles: ['owner', 'hiring-manager', 'recruiter', 'interviewer'] } },\n get: { access: { roles: ['owner', 'hiring-manager', 'recruiter', 'interviewer'] } },\n create: { access: { roles: ['hiring-manager', 'recruiter'] } },\n update: { access: { roles: ['hiring-manager', 'recruiter'] } },\n delete: { access: { roles: ['hiring-manager'] }, mode: 'hard' },\n },\n\n // GUARDS: Field-level protection\n guards: {\n createable: ['name', 'email', 'phone', 'resumeUrl', 'source'],\n updatable: ['name', 'phone'],\n },\n\n // MASKING: PII redaction\n masking: {\n email: { type: 'email', show: { roles: ['hiring-manager', 'recruiter'] } },\n phone: { type: 'phone', show: { roles: ['hiring-manager', 'recruiter'] } },\n },\n\n // VIEWS: Column-level projections\n views: {\n pipeline: {\n fields: ['id', 'name', 'source'],\n access: { roles: ['owner', 'hiring-manager', 'recruiter', 'interviewer'] },\n },\n full: {\n fields: ['id', 'name', 'email', 'phone', 'resumeUrl', 'source', 'internalNotes'],\n access: { roles: ['hiring-manager', 'recruiter'] },\n },\n },\n});\n\nexport type Candidate = typeof candidates.$inferSelect;\n```\n\n---\n\n## What Quickback Generates\n\nRun `quickback compile` and Quickback generates production-ready code.\n\n### Security Helpers\n\n```typescript title=\"src/features/candidates/candidates.resource.ts\"\n\n/**\n * Firewall conditions for candidates\n * Pattern: Organization only\n */\nexport function buildFirewallConditions(ctx: AppContext) {\n const conditions = [];\n\n // Organization isolation\n conditions.push(eq(candidates.organizationId, ctx.activeOrgId!));\n\n return and(...conditions);\n}\n\n/**\n * Guards configuration for field modification rules\n */\nexport const GUARDS_CONFIG = {\n createable: new Set(['name', 'email', 'phone', 'resumeUrl', 'source']),\n updatable: new Set(['name', 'phone']),\n immutable: new Set([]),\n protected: {},\n systemManaged: new Set([\n 'createdAt', 'createdBy',\n 'modifiedAt', 'modifiedBy',\n 'deletedAt', 'deletedBy'\n ]),\n};\n\n/**\n * Validates field input for CREATE operations.\n */\nexport function validateCreate(input: Record<string, any>) {\n const systemManaged: string[] = [];\n const protectedFields: Array<{ field: string; actions: string[] }> = [];\n const notCreateable: string[] = [];\n\n for (const field of Object.keys(input)) {\n if (GUARDS_CONFIG.systemManaged.has(field)) {\n systemManaged.push(field);\n continue;\n }\n if (field in GUARDS_CONFIG.protected) {\n const actions = [...GUARDS_CONFIG.protected[field]];\n protectedFields.push({ field, actions });\n continue;\n }\n if (!GUARDS_CONFIG.createable.has(field) && !GUARDS_CONFIG.immutable.has(field)) {\n notCreateable.push(field);\n }\n }\n\n const valid = systemManaged.length === 0 &&\n protectedFields.length === 0 &&\n notCreateable.length === 0;\n\n return { valid, systemManaged, protected: protectedFields, notCreateable };\n}\n\n/**\n * Validates field input for UPDATE operations.\n */\nexport function validateUpdate(input: Record<string, any>) {\n const systemManaged: string[] = [];\n const immutable: string[] = [];\n const protectedFields: Array<{ field: string; actions: string[] }> = [];\n const notUpdatable: string[] = [];\n\n for (const field of Object.keys(input)) {\n if (GUARDS_CONFIG.systemManaged.has(field)) {\n systemManaged.push(field);\n continue;\n }\n if (GUARDS_CONFIG.immutable.has(field)) {\n immutable.push(field);\n continue;\n }\n if (field in GUARDS_CONFIG.protected) {\n const actions = [...GUARDS_CONFIG.protected[field]];\n protectedFields.push({ field, actions });\n continue;\n }\n if (!GUARDS_CONFIG.updatable.has(field)) {\n notUpdatable.push(field);\n }\n }\n\n const valid = systemManaged.length === 0 &&\n immutable.length === 0 &&\n protectedFields.length === 0 &&\n notUpdatable.length === 0;\n\n return { valid, systemManaged, immutable, protected: protectedFields, notUpdatable };\n}\n\n/**\n * Masks sensitive fields based on user role\n */\nexport function maskCandidate<T extends Record<string, any>>(\n record: T,\n ctx: AppContext\n): T {\n const masked: any = { ...record };\n\n // email: show to hiring-manager, recruiter\n if (!ctx.roles?.some(r => ['hiring-manager', 'recruiter'].includes(r))) {\n if (masked['email'] != null) {\n masked['email'] = masks.email(masked['email']);\n }\n }\n\n // phone: show to hiring-manager, recruiter\n if (!ctx.roles?.some(r => ['hiring-manager', 'recruiter'].includes(r))) {\n if (masked['phone'] != null) {\n masked['phone'] = masks.phone(masked['phone']);\n }\n }\n\n return masked as T;\n}\n\nexport function maskCandidates<T extends Record<string, any>>(\n records: T[],\n ctx: AppContext\n): T[] {\n return records.map(r => maskCandidate(r, ctx));\n}\n\n// CRUD Access configuration\nexport const CRUD_ACCESS = {\n list: { access: { roles: ['owner', 'hiring-manager', 'recruiter', 'interviewer'] } },\n get: { access: { roles: ['owner', 'hiring-manager', 'recruiter', 'interviewer'] } },\n create: { access: { roles: ['hiring-manager', 'recruiter'] } },\n update: { access: { roles: ['hiring-manager', 'recruiter'] } },\n delete: { access: { roles: ['hiring-manager'] }, mode: 'hard' },\n};\n\n// Views configuration\nexport const VIEWS_CONFIG = {\n pipeline: {\n fields: ['id', 'name', 'source'],\n access: { roles: ['owner', 'hiring-manager', 'recruiter', 'interviewer'] },\n },\n full: {\n fields: ['id', 'name', 'email', 'phone', 'resumeUrl', 'source', 'internalNotes'],\n access: { roles: ['hiring-manager', 'recruiter'] },\n },\n};\n```\n\n### API Routes\n\nThe generated routes wire everything together:\n\n```typescript title=\"src/features/candidates/candidates.routes.ts\"\n\n buildFirewallConditions,\n validateCreate,\n validateUpdate,\n maskCandidate,\n maskCandidates,\n CRUD_ACCESS\n} from './candidates.resource';\n\nconst app = new Hono();\n\n// GET /candidates - List with firewall + masking\napp.get('/', async (c) => {\n const ctx = c.get('ctx');\n const db = c.get('db');\n\n // Access check\n if (!await evaluateAccess(CRUD_ACCESS.list.access, ctx)) {\n return c.json(AccessErrors.roleRequired(\n CRUD_ACCESS.list.access.roles,\n ctx.roles\n ), 403);\n }\n\n // Query with firewall conditions\n const results = await db.select().from(candidates)\n .where(buildFirewallConditions(ctx));\n\n // Apply masking before returning\n return c.json({\n data: maskCandidates(results, ctx),\n });\n});\n\n// POST /candidates - Create with guards\napp.post('/', async (c) => {\n const ctx = c.get('ctx');\n const db = c.get('db');\n\n // Access check\n if (!await evaluateAccess(CRUD_ACCESS.create.access, ctx)) {\n return c.json(AccessErrors.roleRequired(\n CRUD_ACCESS.create.access.roles,\n ctx.roles\n ), 403);\n }\n\n const body = await c.req.json();\n\n // Guards validation\n const validation = validateCreate(body);\n if (!validation.valid) {\n if (validation.systemManaged.length > 0) {\n return c.json(GuardErrors.systemManaged(validation.systemManaged), 400);\n }\n if (validation.notCreateable.length > 0) {\n return c.json(GuardErrors.fieldNotCreateable(validation.notCreateable), 400);\n }\n }\n\n // Apply ownership and audit fields\n const data = {\n id: 'cnd_' + crypto.randomUUID().replace(/-/g, ''),\n ...body,\n organizationId: ctx.activeOrgId,\n createdAt: new Date().toISOString(),\n createdBy: ctx.userId,\n modifiedAt: new Date().toISOString(),\n modifiedBy: ctx.userId,\n };\n\n const result = await db.insert(candidates).values(data).returning();\n return c.json(maskCandidate(result[0], ctx), 201);\n});\n\n// PATCH /candidates/:id - Update with guards\napp.patch('/:id', async (c) => {\n const ctx = c.get('ctx');\n const db = c.get('db');\n const id = c.req.param('id');\n\n // Fetch with firewall\n const [record] = await db.select().from(candidates)\n .where(and(buildFirewallConditions(ctx), eq(candidates.id, id)));\n\n if (!record) {\n return c.json({ error: 'Not found', code: 'NOT_FOUND' }, 404);\n }\n\n // Access check\n if (!await evaluateAccess(CRUD_ACCESS.update.access, ctx)) {\n return c.json(AccessErrors.roleRequired(\n CRUD_ACCESS.update.access.roles,\n ctx.roles\n ), 403);\n }\n\n const body = await c.req.json();\n\n // Guards validation\n const validation = validateUpdate(body);\n if (!validation.valid) {\n if (validation.notUpdatable.length > 0) {\n return c.json(GuardErrors.fieldNotUpdatable(validation.notUpdatable), 400);\n }\n }\n\n // Apply audit fields\n const data = {\n ...body,\n modifiedAt: new Date().toISOString(),\n modifiedBy: ctx.userId,\n };\n\n const result = await db.update(candidates).set(data)\n .where(eq(candidates.id, id)).returning();\n\n return c.json(maskCandidate(result[0], ctx));\n});\n\nexport default app;\n```\n\n---\n\n## Runtime Behavior\n\n### Masking by Role\n\n| Role | `email` | `phone` |\n|------|---------|---------|\n| **hiring-manager** | `jane@example.com` | `555-123-4567` |\n| **recruiter** | `jane@example.com` | `555-123-4567` |\n| **interviewer** | `j***@e******.com` | `******4567` |\n\n### Views + Masking Interaction\n\nViews control **which fields are returned**. Masking controls **what values are shown**.\n\n| Endpoint | Role | `phone` in response? | `phone` value |\n|----------|------|--------------------|-------------|\n| `/candidates/views/pipeline` | interviewer | No | N/A |\n| `/candidates/views/pipeline` | recruiter | No | N/A |\n| `/candidates/views/full` | interviewer | No | N/A (no access) |\n| `/candidates/views/full` | recruiter | Yes | `555-123-4567` |\n\n### Guards Enforcement\n\n```bash\n# Allowed: Create with createable fields\nPOST /candidates\n{ \"name\": \"Jane Smith\", \"email\": \"jane@example.com\", \"source\": \"linkedin\" }\n\n# Rejected: Update non-updatable field\nPATCH /candidates/cnd_123\n{ \"email\": \"new@example.com\" }\n# -> 400: \"Field 'email' is not updatable\"\n\n# Rejected: Set system-managed field\nPOST /candidates\n{ \"name\": \"Jane Smith\", \"createdBy\": \"hacker\" }\n# -> 400: \"System-managed fields cannot be set: createdBy\"\n```\n\n---\n\n## Generated API Endpoints\n\n| Method | Endpoint | Access |\n|--------|----------|--------|\n| `GET` | `/candidates` | owner, hiring-manager, recruiter, interviewer |\n| `GET` | `/candidates/:id` | owner, hiring-manager, recruiter, interviewer |\n| `POST` | `/candidates` | hiring-manager, recruiter |\n| `PATCH` | `/candidates/:id` | hiring-manager, recruiter |\n| `DELETE` | `/candidates/:id` | hiring-manager |\n| `GET` | `/candidates/views/pipeline` | owner, hiring-manager, recruiter, interviewer |\n| `GET` | `/candidates/views/full` | hiring-manager, recruiter |\n\n---\n\n## Key Takeaways\n\n1. **~50 lines of configuration** generates **~500+ lines of production code**\n2. **Security is declarative** - you describe *what* you want, not *how*\n3. **All layers work together** - Firewall -> Access -> Guards -> Masking\n4. **Code is readable and auditable** - no magic, just TypeScript\n5. **Type-safe from definition to API** - Drizzle schema drives everything"
191
195
  },
192
196
  "compiler/getting-started/hand-crafted": {
193
197
  "title": "Hand-Crafted Setup",
194
- "content": "If you have an existing project and want to add Quickback-generated API endpoints, you can set up the definitions directory manually instead of using a template.\n\n## Prerequisites\n\n- An existing Hono-based project (Cloudflare Workers or Bun)\n- Node.js 18+ or Bun installed\n- The Quickback CLI: `npm install -g @kardoe/quickback`\n\n## Setup\n\n### 1. Create the Quickback Directory\n\nCreate a `quickback/` directory in your project root with the following structure:\n\n```\nyour-project/\n├── quickback/\n│ ├── quickback.config.ts\n│ └── features/\n│ └── (your features go here)\n├── src/ # Your existing code\n├── package.json\n└── ...\n```\n\n```bash\nmkdir -p quickback/features\n```\n\n### 2. Write Your Config\n\nCreate `quickback/quickback.config.ts`:\n\n```typescript\n\nexport default defineConfig({\n name: \"my-app\",\n template: \"hono\",\n features: [\"organizations\"],\n providers: {\n runtime: { name: \"cloudflare\", config: {} },\n database: {\n name: \"cloudflare-d1\",\n config: { binding: \"DB\" },\n },\n auth: { name: \"better-auth\", config: {} },\n },\n});\n```\n\nAdjust the providers to match your existing stack. See [Providers](/compiler/config/providers) for all options.\n\n### 3. Create Your First Feature\n\nCreate a feature directory with a schema + security definition:\n\n```bash\nmkdir quickback/features/customers\n```\n\nCreate `quickback/features/customers/customers.ts`:\n\n```typescript\n\nexport const customers = sqliteTable(\"customers\", {\n id: text(\"id\").primaryKey(),\n name: text(\"name\").notNull(),\n email: text(\"email\").notNull(),\n status: text(\"status\").default(\"active\"),\n organizationId: text(\"organization_id\").notNull(),\n});\n\nexport default defineTable(customers, {\n firewall: {\n organization: {},\n },\n guards: {\n createable: [\"name\", \"email\", \"status\"],\n updatable: [\"name\", \"email\", \"status\"],\n immutable: [\"id\", \"organizationId\"],\n },\n masking: {\n email: { type: \"email\", show: { roles: [\"admin\"] } },\n },\n crud: {\n list: { access: { roles: [\"owner\", \"admin\", \"member\"] } },\n get: { access: { roles: [\"owner\", \"admin\", \"member\"] } },\n create: { access: { roles: [\"owner\", \"admin\"] } },\n update: { access: { roles: [\"owner\", \"admin\"] } },\n delete: { access: { roles: [\"owner\", \"admin\"] }, mode: \"soft\" },\n },\n});\n```\n\n### 4. Log In and Compile\n\n```bash\nquickback login\nquickback compile\n```\n\nThe compiler generates a complete `src/` directory with route handlers, middleware, database schemas, and migrations.\n\n### 5. Integrate with Your Existing Code\n\nThe compiled output creates a self-contained Hono app in `src/index.ts`. If you need to integrate the generated routes into an existing Hono app, you can import the feature routes directly:\n\n```typescript\n\nconst app = new Hono();\n\n// Your existing routes\napp.get(\"/\", (c) => c.json({ status: \"ok\" }));\n\n// Mount generated feature routes\napp.route(\"/api/v1/customers\", customersRoutes);\n\nexport default app;\n```\n\n## Directory Structure\n\nThe compiler expects this structure inside `quickback/`:\n\n```\nquickback/\n├── quickback.config.ts # Required: compiler configuration\n└── features/ # Required: feature definitions\n ├── customers/\n │ ├── customers.ts # Schema + security (defineTable)\n │ └── actions.ts # Optional: custom actions\n ├── orders/\n │ ├── orders.ts\n │ └── actions.ts\n └── ...\n```\n\nEach feature directory should contain:\n- **`{name}.ts`** — The main definition file using `defineTable()` (required)\n- **`actions.ts`** — Custom actions using `defineActions()` (optional)\n\n## Adding Features\n\nTo add a new feature, create a new directory under `quickback/features/` and recompile:\n\n```bash\nmkdir quickback/features/invoices\n# Create invoices/invoices.ts with defineTable(...)\nquickback compile\n```\n\nThe compiler detects all features automatically — no registration needed.\n\n## Recompiling\n\nAfter any change to your definitions, recompile to regenerate the output:\n\n```bash\nquickback compile\n```\n\nThe compiler regenerates the entire `src/` directory. Your definitions in `quickback/` are the source of truth — never edit the generated files directly.\n\n**Warning:** Don't edit files in `src/` manually. They will be overwritten on the next compile. All changes should be made in your `quickback/` definitions.\n\n## Next Steps\n\n- [Configuration reference](/compiler/config) — All config options\n- [Schema definitions](/compiler/definitions/schema) — Define tables with `defineTable()`\n- [Security pillars](/compiler/definitions) — Firewall, Access, Guards, Masking\n- [Templates](/compiler/getting-started/templates) — Use a template for new projects instead"
198
+ "content": "If you have an existing project and want to add Quickback-generated API endpoints, you can set up the definitions directory manually instead of using a template.\n\n## Prerequisites\n\n- An existing Hono-based project (Cloudflare Workers or Bun)\n- Node.js 18+ or Bun installed\n- The Quickback CLI: `npm install -g @kardoe/quickback`\n\n## Setup\n\n### 1. Create the Quickback Directory\n\nCreate a `quickback/` directory in your project root with the following structure:\n\n```\nyour-project/\n├── quickback/\n│ ├── quickback.config.ts\n│ └── features/\n│ └── (your features go here)\n├── src/ # Your existing code\n├── package.json\n└── ...\n```\n\n```bash\nmkdir -p quickback/features\n```\n\n### 2. Write Your Config\n\nCreate `quickback/quickback.config.ts`:\n\n```typescript\n\nexport default defineConfig({\n name: \"my-app\",\n template: \"hono\",\n features: [\"organizations\"],\n providers: {\n runtime: { name: \"cloudflare\", config: {} },\n database: {\n name: \"cloudflare-d1\",\n config: { binding: \"DB\" },\n },\n auth: { name: \"better-auth\", config: {} },\n },\n});\n```\n\nAdjust the providers to match your existing stack. See [Providers](/compiler/config/providers) for all options.\n\n### 3. Create Your First Feature\n\nCreate a feature directory with a schema + security definition:\n\n```bash\nmkdir quickback/features/candidates\n```\n\nCreate `quickback/features/candidates/candidates.ts`:\n\n```typescript\n\nexport const candidates = sqliteTable(\"candidates\", {\n id: text(\"id\").primaryKey(),\n name: text(\"name\").notNull(),\n email: text(\"email\").notNull(),\n phone: text(\"phone\"),\n source: text(\"source\"),\n organizationId: text(\"organization_id\").notNull(),\n});\n\nexport default defineTable(candidates, {\n firewall: {\n organization: {},\n },\n guards: {\n createable: [\"name\", \"email\", \"phone\", \"source\"],\n updatable: [\"name\", \"phone\"],\n },\n masking: {\n email: { type: \"email\", show: { roles: [\"hiring-manager\", \"recruiter\"] } },\n phone: { type: \"phone\", show: { roles: [\"hiring-manager\", \"recruiter\"] } },\n },\n crud: {\n list: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\", \"interviewer\"] } },\n get: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\", \"interviewer\"] } },\n create: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\"] } },\n update: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\"] } },\n delete: { access: { roles: [\"owner\", \"hiring-manager\"] }, mode: \"soft\" },\n },\n});\n```\n\n### 4. Log In and Compile\n\n```bash\nquickback login\nquickback compile\n```\n\nThe compiler generates a complete `src/` directory with route handlers, middleware, database schemas, and migrations.\n\n### 5. Integrate with Your Existing Code\n\nThe compiled output creates a self-contained Hono app in `src/index.ts`. If you need to integrate the generated routes into an existing Hono app, you can import the feature routes directly:\n\n```typescript\n\nconst app = new Hono();\n\n// Your existing routes\napp.get(\"/\", (c) => c.json({ status: \"ok\" }));\n\n// Mount generated feature routes\napp.route(\"/api/v1/candidates\", candidatesRoutes);\n\nexport default app;\n```\n\n## Directory Structure\n\nThe compiler expects this structure inside `quickback/`:\n\n```\nquickback/\n├── quickback.config.ts # Required: compiler configuration\n└── features/ # Required: feature definitions\n ├── candidates/\n │ ├── candidates.ts # Schema + security (defineTable)\n │ └── actions.ts # Optional: custom actions\n ├── jobs/\n │ ├── jobs.ts\n │ └── actions.ts\n └── ...\n```\n\nEach feature directory should contain:\n- **`{name}.ts`** — The main definition file using `defineTable()` (required)\n- **`actions.ts`** — Custom actions using `defineActions()` (optional)\n\n## Adding Features\n\nTo add a new feature, create a new directory under `quickback/features/` and recompile:\n\n```bash\nmkdir quickback/features/jobs\n# Create jobs/jobs.ts with defineTable(...)\nquickback compile\n```\n\nThe compiler detects all features automatically — no registration needed.\n\n## Recompiling\n\nAfter any change to your definitions, recompile to regenerate the output:\n\n```bash\nquickback compile\n```\n\nThe compiler regenerates the entire `src/` directory. Your definitions in `quickback/` are the source of truth — never edit the generated files directly.\n\n**Warning:** Don't edit files in `src/` manually. They will be overwritten on the next compile. All changes should be made in your `quickback/` definitions.\n\n## Next Steps\n\n- [Configuration reference](/compiler/config) — All config options\n- [Schema definitions](/compiler/definitions/schema) — Define tables with `defineTable()`\n- [Security pillars](/compiler/definitions) — Firewall, Access, Guards, Masking\n- [Templates](/compiler/getting-started/templates) — Use a template for new projects instead"
195
199
  },
196
200
  "compiler/getting-started": {
197
201
  "title": "Getting Started",
198
- "content": "Get started with Quickback in minutes. This guide shows you how to define a complete table with security configuration.\n\n## Install the CLI\n\n```bash\nnpm install -g @kardoe/quickback\n```\n\n## Create a Project\n\n```bash\nquickback create cloudflare my-app\ncd my-app\n```\n\nThis scaffolds a complete project with:\n- `quickback.config.ts` — Project configuration\n- `definitions/features/` — Your table definitions\n- Example todos feature with full security configuration\n\n**Available templates:**\n- `cloudflare` — Cloudflare Workers + D1 + Better Auth (free)\n- `bun` — Bun + SQLite + Better Auth (free)\n- `turso` — Turso/LibSQL + Better Auth (pro)\n\n## File Structure\n\nEach table gets its own file with schema and config together using `defineTable`:\n\n```\ndefinitions/\n└── features/\n └── rooms/\n ├── rooms.ts # Table + security config\n ├── room-bookings.ts # Related table + config\n ├── actions.ts # Custom actions (optional)\n └── handlers/ # Action handlers (optional)\n └── activate.ts\n```\n\n## Complete Example\n\nHere's a complete `rooms` table with all security layers:\n\n```typescript\n// definitions/features/rooms/rooms.ts\n\nexport const rooms = sqliteTable('rooms', {\n id: text('id').primaryKey(),\n name: text('name').notNull(),\n description: text('description'),\n capacity: integer('capacity').notNull().default(10),\n roomType: text('room_type').notNull(),\n isActive: integer('is_active', { mode: 'boolean' }).notNull().default(true),\n deactivationReason: text('deactivation_reason'),\n\n // Ownership - required for firewall data isolation\n organizationId: text('organization_id').notNull(),\n});\n\nexport default defineTable(rooms, {\n // 1. FIREWALL - Data isolation\n firewall: { organization: {} },\n\n // 2. GUARDS - Field modification rules\n guards: {\n createable: [\"name\", \"description\", \"capacity\", \"roomType\"],\n updatable: [\"name\", \"description\", \"capacity\"],\n protected: {\n isActive: [\"activate\", \"deactivate\"], // Only via actions\n },\n },\n\n // 3. CRUD - Role-based access control\n crud: {\n list: { access: { roles: [\"owner\", \"admin\", \"member\"] }, pageSize: 25 },\n get: { access: { roles: [\"owner\", \"admin\", \"member\"] } },\n create: { access: { roles: [\"owner\", \"admin\"] } },\n update: { access: { roles: [\"owner\", \"admin\"] } },\n delete: { access: { roles: [\"owner\", \"admin\"] }, mode: \"soft\" },\n },\n});\n\nexport type Room = typeof rooms.$inferSelect;\n```\n\n## What Each Layer Does\n\n1. **Firewall**: Automatically adds `WHERE organizationId = ?` to every query. Users in Org A can never see Org B's data.\n2. **Guards**: Controls which fields can be modified — `createable` for POST, `updatable` for PATCH, `protected` for action-only fields.\n3. **CRUD Access**: Role-based access control for each operation. Members can read, only admins can write.\n\n## Compile and Run\n\n```bash\n# Log in (first time only)\nquickback login\n\n# Compile your definitions\nquickback compile\n\n# Run locally\nnpm run dev\n```\n\n## Generated Endpoints\n\nQuickback generates these endpoints from the example above:\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| `GET` | `/api/v1/rooms` | List rooms (owners, admins, members) |\n| `GET` | `/api/v1/rooms/:id` | Get single room |\n| `POST` | `/api/v1/rooms` | Create room (admins only) |\n| `PATCH` | `/api/v1/rooms/:id` | Update room (admins only) |\n| `DELETE` | `/api/v1/rooms/:id` | Soft delete room (admins only) |\n\n## Next Steps\n\n- [Template Walkthroughs](/compiler/getting-started/templates) — Detailed setup guides\n- [Full Example](/compiler/getting-started/full-example) — Complete resource walkthrough\n- [Database Schema](/compiler/definitions/schema) — Column types, relations, audit fields\n- [Firewall](/compiler/definitions/firewall) — Data isolation patterns\n- [Access](/compiler/definitions/access) — Role & condition-based control\n- [Guards](/compiler/definitions/guards) — Field modification rules\n- [Masking](/compiler/definitions/masking) — Field redaction for sensitive data\n- [Actions](/compiler/definitions/actions) — Custom business logic endpoints"
202
+ "content": "Get started with Quickback in minutes. This guide shows you how to define a complete table with security configuration.\n\n## Install the CLI\n\n```bash\nnpm install -g @kardoe/quickback\n```\n\n## Create a Project\n\n```bash\nquickback create cloudflare my-app\ncd my-app\n```\n\nThis scaffolds a complete project with:\n- `quickback.config.ts` — Project configuration\n- `quickback/features/` — Your table definitions\n- Example feature with full security configuration\n\n**Available templates:**\n- `cloudflare` — Cloudflare Workers + D1 + Better Auth (free)\n- `bun` — Bun + SQLite + Better Auth (free)\n- `turso` — Turso/LibSQL + Better Auth (pro)\n\n## File Structure\n\nEach table gets its own file with schema and config together using `defineTable`:\n\n```\nquickback/\n├── quickback.config.ts\n└── features/\n └── jobs/\n ├── jobs.ts # Table + security config\n ├── applications.ts # Related table + config\n ├── actions.ts # Custom actions (optional)\n └── handlers/ # Action handlers (optional)\n └── close-job.ts\n```\n\n## Complete Example\n\nHere's a complete `jobs` table with all security layers:\n\n```typescript\n// quickback/features/jobs/jobs.ts\n\nexport const jobs = sqliteTable('jobs', {\n id: text('id').primaryKey(),\n title: text('title').notNull(),\n department: text('department').notNull(),\n status: text('status').notNull().default('draft'), // draft, open, closed\n salaryMin: integer('salary_min'),\n salaryMax: integer('salary_max'),\n\n // Ownership - required for firewall data isolation\n organizationId: text('organization_id').notNull(),\n});\n\nexport default defineTable(jobs, {\n // 1. FIREWALL - Data isolation\n firewall: { organization: {} },\n\n // 2. GUARDS - Field modification rules\n guards: {\n createable: [\"title\", \"department\", \"status\", \"salaryMin\", \"salaryMax\"],\n updatable: [\"title\", \"department\", \"status\"],\n },\n\n // 3. CRUD - Role-based access control\n crud: {\n list: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\", \"interviewer\"] }, pageSize: 25 },\n get: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\", \"interviewer\"] } },\n create: { access: { roles: [\"owner\", \"hiring-manager\"] } },\n update: { access: { roles: [\"owner\", \"hiring-manager\"] } },\n delete: { access: { roles: [\"owner\", \"hiring-manager\"] }, mode: \"soft\" },\n },\n});\n\nexport type Job = typeof jobs.$inferSelect;\n```\n\n## What Each Layer Does\n\n1. **Firewall**: Automatically adds `WHERE organizationId = ?` to every query. Users in Org A can never see Org B's data.\n2. **Guards**: Controls which fields can be modified — `createable` for POST, `updatable` for PATCH, `protected` for action-only fields.\n3. **CRUD Access**: Role-based access control for each operation. All roles can read, only hiring managers can write.\n\n## Compile and Run\n\n```bash\n# Log in (first time only)\nquickback login\n\n# Compile your definitions\nquickback compile\n\n# Run locally\nnpm run dev\n```\n\n## Generated Endpoints\n\nQuickback generates these endpoints from the example above:\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| `GET` | `/api/v1/jobs` | List jobs (all roles) |\n| `GET` | `/api/v1/jobs/:id` | Get single job |\n| `POST` | `/api/v1/jobs` | Create job (hiring managers only) |\n| `PATCH` | `/api/v1/jobs/:id` | Update job (hiring managers only) |\n| `DELETE` | `/api/v1/jobs/:id` | Soft delete job (hiring managers only) |\n\n## Test Your API\n\nAfter `quickback compile` and `npm run dev`, your API is running locally. Open a second terminal and try these requests:\n\n```bash\n# 1. Create a user account\ncurl -X POST http://localhost:8787/api/auth/sign-up/email \\\n -H \"Content-Type: application/json\" \\\n -d '{\"email\": \"admin@example.com\", \"password\": \"securepassword123\", \"name\": \"Admin\"}'\n\n# 2. Sign in and get a session token\ncurl -X POST http://localhost:8787/api/auth/sign-in/email \\\n -H \"Content-Type: application/json\" \\\n -d '{\"email\": \"admin@example.com\", \"password\": \"securepassword123\"}'\n# → Response includes a session token in Set-Cookie header\n\n# 3. Create a record (use the session cookie from step 2)\ncurl -X POST http://localhost:8787/api/v1/jobs \\\n -H \"Content-Type: application/json\" \\\n -H \"Cookie: better-auth.session_token=<token>\" \\\n -d '{\"title\": \"Senior Engineer\", \"department\": \"Engineering\", \"status\": \"open\"}'\n\n# 4. List records\ncurl http://localhost:8787/api/v1/jobs \\\n -H \"Cookie: better-auth.session_token=<token>\"\n```\n\n## Next Steps\n\n- [Template Walkthroughs](/compiler/getting-started/templates) — Detailed setup guides\n- [Full Example](/compiler/getting-started/full-example) — Complete resource walkthrough\n- [Database Schema](/compiler/definitions/schema) — Column types, relations, audit fields\n- [Firewall](/compiler/definitions/firewall) — Data isolation patterns\n- [Access](/compiler/definitions/access) — Role & condition-based control\n- [Guards](/compiler/definitions/guards) — Field modification rules\n- [Masking](/compiler/definitions/masking) — Field redaction for sensitive data\n- [Actions](/compiler/definitions/actions) — Custom business logic endpoints\n\n## See Also\n\n- [Quickback Stack](/stack) — The runtime environment where your compiled API runs (D1, KV, R2, auth)\n- [Account UI](/account-ui) — Pre-built authentication and account management UI\n- [Using the API](/compiler/using-the-api) — CRUD endpoints, filtering, and batch operations"
203
+ },
204
+ "compiler/getting-started/patterns": {
205
+ "title": "Common Patterns",
206
+ "content": "Quick recipes for common scenarios. Each pattern shows the complete `defineTable` configuration.\n\n## Public Read, Authenticated Write\n\nA job board where all team members can browse open positions but only hiring managers can create or edit jobs:\n\n```typescript\n// quickback/features/jobs/jobs.ts\nexport default defineTable(jobs, {\n firewall: { organization: {} },\n guards: {\n createable: [\"title\", \"department\", \"status\", \"salaryMin\", \"salaryMax\"],\n updatable: [\"title\", \"department\", \"status\"],\n },\n crud: {\n list: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\", \"interviewer\"] } },\n get: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\", \"interviewer\"] } },\n create: { access: { roles: [\"owner\", \"hiring-manager\"] } },\n update: { access: { roles: [\"owner\", \"hiring-manager\"] } },\n delete: { access: { roles: [\"owner\", \"hiring-manager\"] } },\n },\n});\n```\n\n## User-Scoped Personal Data\n\nInterview notes that belong to a specific interviewer. Each interviewer only sees their own notes:\n\n```typescript\n// Schema includes both organizationId AND ownerId\nexport const interviewNotes = sqliteTable(\"interview_notes\", {\n id: text(\"id\").primaryKey(),\n organizationId: text(\"organization_id\").notNull(),\n ownerId: text(\"owner_id\").notNull(),\n candidateId: text(\"candidate_id\").notNull(),\n content: text(\"content\").notNull(),\n rating: integer(\"rating\"),\n});\n\nexport default defineTable(interviewNotes, {\n firewall: {\n organization: {},\n owner: { mode: \"strict\" }, // Only the interviewer can see their own notes\n },\n guards: {\n createable: [\"candidateId\", \"content\", \"rating\"],\n updatable: [\"content\", \"rating\"],\n },\n crud: {\n list: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\", \"interviewer\"] } },\n get: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\", \"interviewer\"] } },\n create: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\", \"interviewer\"] } },\n update: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\", \"interviewer\"] } },\n delete: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\", \"interviewer\"] } },\n },\n});\n```\n\nWith `owner.mode: \"strict\"`, even hiring managers can only see their own notes. Use `\"optional\"` to let hiring managers see all notes.\n\n## Multi-Table Feature with Internal Tables\n\nA feature with a main table exposed via API and a junction table used only in actions:\n\n```typescript\n// quickback/features/applications/applications.ts — exposed via API\nexport default defineTable(applications, {\n firewall: { organization: {} },\n guards: {\n createable: [\"candidateId\", \"jobId\", \"stage\", \"notes\"],\n updatable: [\"notes\"],\n },\n crud: {\n list: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\", \"interviewer\"] } },\n get: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\", \"interviewer\"] } },\n create: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\"] } },\n update: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\"] } },\n delete: { access: { roles: [\"owner\", \"hiring-manager\"] } },\n },\n});\n```\n\n```typescript\n// quickback/features/applications/interview-scores.ts — internal, no API routes\n// No default export = no routes generated\nexport const interviewScores = sqliteTable(\"interview_scores\", {\n applicationId: text(\"application_id\").notNull(),\n interviewerId: text(\"interviewer_id\").notNull(),\n score: integer(\"score\").notNull(),\n feedback: text(\"feedback\"),\n organizationId: text(\"organization_id\").notNull(), // Always scope junction tables\n});\n```\n\n```typescript\n// quickback/features/applications/actions.ts — uses the internal table\n\nexport default defineActions(applications, {\n submitScore: {\n type: \"record\",\n input: z.object({ score: z.number().min(1).max(5), feedback: z.string() }),\n access: { roles: [\"owner\", \"hiring-manager\", \"interviewer\"] },\n execute: async ({ db, record, input, ctx }) => {\n await db.insert(interviewScores).values({\n applicationId: record.id,\n interviewerId: ctx.userId,\n score: input.score,\n feedback: input.feedback,\n });\n return record;\n },\n },\n});\n```\n\n## Admin-Only Status Field\n\nA field that only hiring managers can modify, but everyone can see:\n\n```typescript\nexport default defineTable(applications, {\n firewall: { organization: {} },\n guards: {\n createable: [\"candidateId\", \"jobId\", \"notes\"],\n updatable: [\"notes\"], // Recruiters can only update notes\n protected: [\"stage\"], // Stage can only be changed via actions\n },\n crud: {\n list: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\", \"interviewer\"] } },\n get: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\", \"interviewer\"] } },\n create: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\"] } },\n update: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\"] } },\n delete: { access: { roles: [\"hiring-manager\"] } },\n },\n});\n```\n\nThen create an action that hiring managers use to change stage:\n\n```typescript\n// quickback/features/applications/actions.ts\nexport default defineActions(applications, {\n advanceStage: {\n type: \"record\",\n input: z.object({ stage: z.enum([\"applied\", \"screening\", \"interview\", \"offer\", \"hired\", \"rejected\"]) }),\n access: { roles: [\"hiring-manager\"] },\n execute: async ({ db, record, input }) => {\n await db.update(applications).set({ stage: input.stage }).where(eq(applications.id, record.id));\n return { ...record, stage: input.stage };\n },\n },\n});\n```\n\n## Sensitive Data with Role-Based Masking\n\nA candidate record where PII is masked differently per role:\n\n```typescript\nexport default defineTable(candidates, {\n firewall: { organization: {} },\n guards: {\n createable: [\"name\", \"email\", \"phone\", \"resumeUrl\", \"source\"],\n updatable: [\"name\", \"phone\"],\n },\n masking: {\n email: { type: \"email\", show: { roles: [\"hiring-manager\", \"recruiter\"] } },\n phone: { type: \"phone\", show: { roles: [\"hiring-manager\", \"recruiter\"] } },\n },\n crud: {\n list: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\", \"interviewer\"] } },\n get: { access: { roles: [\"owner\", \"hiring-manager\", \"recruiter\", \"interviewer\"] } },\n create: { access: { roles: [\"hiring-manager\", \"recruiter\"] } },\n update: { access: { roles: [\"hiring-manager\", \"recruiter\"] } },\n delete: { access: { roles: [\"hiring-manager\"] } },\n },\n});\n```\n\n| Role | email | phone |\n|------|-------|-------|\n| hiring-manager | `jane@example.com` | `555-123-4567` |\n| recruiter | `jane@example.com` | `555-123-4567` |\n| interviewer | `j***@e******.com` | `***-***-4567` |\n\n## External System Sync\n\nAn import table for syncing candidates from an external ATS via PUT/upsert:\n\n```typescript\n// quickback/features/ats-imports/ats-imports.ts\nexport default defineTable(atsImports, {\n firewall: { organization: {} },\n guards: {\n createable: [\"externalId\", \"source\", \"candidateName\", \"candidateEmail\", \"rawPayload\"],\n updatable: [\"candidateName\", \"candidateEmail\", \"rawPayload\", \"syncedAt\"],\n },\n crud: {\n list: { access: { roles: [\"owner\", \"hiring-manager\"] } },\n get: { access: { roles: [\"owner\", \"hiring-manager\"] } },\n create: { access: { roles: [\"owner\", \"hiring-manager\"] } },\n update: { access: { roles: [\"owner\", \"hiring-manager\"] } },\n delete: { access: { roles: [\"owner\"] } },\n },\n});\n```\n\n## See Also\n\n- [Full Example](/compiler/getting-started/full-example) — Complete walkthrough with generated code\n- [Firewall](/compiler/definitions/firewall) — All data isolation options\n- [Actions](/compiler/definitions/actions) — Custom business logic"
199
207
  },
200
208
  "compiler/getting-started/template-bun": {
201
209
  "title": "Bun Template",
@@ -207,15 +215,15 @@ export const DOCS = {
207
215
  },
208
216
  "compiler/getting-started/templates": {
209
217
  "title": "Templates",
210
- "content": "Quickback provides pre-configured templates to get you started quickly. Each template combines a runtime, database, and auth provider into a working project with an example feature.\n\n## Available Templates\n\n| Template | Runtime | Database | Auth | Command |\n|----------|---------|----------|------|---------|\n| `cloudflare` | Cloudflare Workers | D1 (SQLite) | Better Auth | `quickback create cloudflare my-app` |\n| `bun` | Bun | better-sqlite3 | Better Auth | `quickback create bun my-app` |\n| `turso` | Bun | LibSQL (Turso) | Better Auth | `quickback create turso my-app` |\n\n## What Each Template Includes\n\nEvery template scaffolds a complete project with:\n\n- **`quickback.config.ts`** — Pre-configured with the right providers\n- **`definitions/features/`** — Example `todos` feature with full security configuration\n- **Database migrations** — Ready to apply\n- **Deployment scripts** — `npm run deploy` for Cloudflare, `npm start` for Bun\n\n## Choosing a Template\n\n### Cloudflare (Recommended for Production)\n\nBest for production deployments. Runs on Cloudflare's global edge network with zero cold starts. Includes D1 (SQLite at the edge), KV storage, and R2 file storage.\n\n- Free tier available (Workers free plan)\n- Global edge deployment\n- Built-in KV, R2, Queues, Vectorize\n- Dual database mode (separate auth and features DBs)\n\n### Bun (Best for Local Development)\n\nBest for local development and prototyping. Runs on Bun with a local SQLite file. No cloud account needed.\n\n- No cloud setup required\n- Fast local iteration\n- SQLite file stored in `data/` directory\n- Easy to switch to Cloudflare later\n\n### Turso (Best for Multi-Region)\n\nBest when you need SQLite with multi-region replication. Uses LibSQL via Turso's managed service.\n\n- Multi-region database replication\n- SQLite compatibility\n- Managed backups and branching\n\n## After Creating a Project\n\n```bash\n# 1. Create the project\nquickback create cloudflare my-app\ncd my-app\n\n# 2. Log in to the compiler\nquickback login\n\n# 3. Compile your definitions\nquickback compile\n\n# 4. Run locally\nnpm run dev\n```\n\nSee the individual template guides for detailed setup:\n- [Cloudflare Template](/compiler/getting-started/template-cloudflare)\n- [Bun Template](/compiler/getting-started/template-bun)"
218
+ "content": "Quickback provides pre-configured templates to get you started quickly. Each template combines a runtime, database, and auth provider into a working project with an example feature.\n\n## Available Templates\n\n| Template | Runtime | Database | Auth | Command |\n|----------|---------|----------|------|---------|\n| `cloudflare` | Cloudflare Workers | D1 (SQLite) | Better Auth | `quickback create cloudflare my-app` |\n| `bun` | Bun | better-sqlite3 | Better Auth | `quickback create bun my-app` |\n| `turso` | Bun | LibSQL (Turso) | Better Auth | `quickback create turso my-app` |\n\n## What Each Template Includes\n\nEvery template scaffolds a complete project with:\n\n- **`quickback.config.ts`** — Pre-configured with the right providers\n- **`quickback/features/`** — Example `todos` feature with full security configuration\n- **Database migrations** — Ready to apply\n- **Deployment scripts** — `npm run deploy` for Cloudflare, `npm start` for Bun\n\n## Choosing a Template\n\n### Cloudflare (Recommended for Production)\n\nBest for production deployments. Runs on Cloudflare's global edge network with zero cold starts. Includes D1 (SQLite at the edge), KV storage, and R2 file storage.\n\n- Free tier available (Workers free plan)\n- Global edge deployment\n- Built-in KV, R2, Queues, Vectorize\n- Dual database mode (separate auth and features DBs)\n\n### Bun (Best for Local Development)\n\nBest for local development and prototyping. Runs on Bun with a local SQLite file. No cloud account needed.\n\n- No cloud setup required\n- Fast local iteration\n- SQLite file stored in `data/` directory\n- Easy to switch to Cloudflare later\n\n### Turso (Best for Multi-Region)\n\nBest when you need SQLite with multi-region replication. Uses LibSQL via Turso's managed service.\n\n- Multi-region database replication\n- SQLite compatibility\n- Managed backups and branching\n\n## After Creating a Project\n\n```bash\n# 1. Create the project\nquickback create cloudflare my-app\ncd my-app\n\n# 2. Log in to the compiler\nquickback login\n\n# 3. Compile your definitions\nquickback compile\n\n# 4. Run locally\nnpm run dev\n```\n\nSee the individual template guides for detailed setup:\n- [Cloudflare Template](/compiler/getting-started/template-cloudflare)\n- [Bun Template](/compiler/getting-started/template-bun)"
211
219
  },
212
220
  "compiler": {
213
221
  "title": "Quickback Compiler",
214
- "content": "The Quickback compiler transforms your declarative resource definitions into a complete, production-ready API. It analyzes your TypeScript definitions at build time, generates optimized code, validates your security configuration, and creates database migrations.\n\n## What the Compiler Does\n\nWhen you run `quickback compile`, the compiler:\n\n1. **Reads your definitions** - Analyzes all `defineTable()` configurations in your `definitions/` folder\n2. **Validates security** - Checks that firewall, guards, access, and masking are properly configured\n3. **Generates API routes** - Creates REST endpoints for each resource (GET, POST, PATCH, DELETE, plus batch operations)\n4. **Generates actions** - Creates custom endpoints from your `defineActions()` definitions\n5. **Creates middleware** - Generates authentication, authorization, and data validation logic\n6. **Generates TypeScript types** - Creates type-safe interfaces for your API\n7. **Generates migrations** - Automatically runs `drizzle-kit generate` to create database migration files\n\n**Input:**\n```\nquickback/\n├── quickback.config.ts # Compiler configuration\n└── definitions/\n └── features/\n └── rooms/\n ├── rooms.ts # defineTable(...)\n └── actions.ts # defineActions(...)\n```\n\n**Output:**\n```\nsrc/\n├── routes/\n│ └── rooms.ts # Generated API handlers\n├── middleware/\n│ ├── auth.ts # Authentication logic\n│ └── firewall.ts # Data isolation queries\n└── types/\n └── rooms.ts # TypeScript interfaces\n```\n\n## Basic Usage\n\n### Compile Your Project\n\n```bash\nquickback compile\n```\n\nThis command:\n- Reads your `quickback.config.ts`\n- Analyzes all resource definitions\n- Generates the complete API codebase\n- Reports any configuration errors\n\n### After Compilation\n\nOnce compilation succeeds, you need to:\n\n1. **Apply migrations:**\n\n Run the database migration for your provider. The generated `package.json` includes the appropriate migration script for your setup.\n\n > **Note:** The compiler automatically runs `drizzle-kit generate` during compilation, so you only need to apply the migrations.\n\n2. **Deploy your API:**\n ```bash\n npm run deploy # Cloudflare Workers\n # or\n npm start # Local development\n ```\n\n## The Compilation Process\n\n### 1. Configuration Loading\n\nThe compiler reads your `quickback.config.ts`:\n\n```typescript\n\nexport default defineConfig({\n name: 'my-app',\n template: 'hono',\n features: ['organizations'],\n providers: {\n runtime: defineRuntime('cloudflare'),\n database: defineDatabase('cloudflare-d1'),\n auth: defineAuth('better-auth'),\n },\n});\n```\n\nThis determines:\n- Which application template to use (`hono` or experimental `nextjs`)\n- Which database adapter to use (D1, SQLite, Turso, etc.)\n- Which authentication system to integrate\n- Which runtime to target (Cloudflare Workers, Bun, Node.js)\n- Which auth plugins to enable (based on `features`)\n\n### 2. Resource Discovery\n\nThe compiler scans `definitions/features/` and identifies:\n\n**Resources (with routes):**\n```typescript\n// definitions/features/rooms/rooms.ts\nexport default defineTable(rooms, { ... });\n// → Generates: GET/POST/PATCH/DELETE /api/v1/rooms\n// → Also generates: POST/PATCH/DELETE/PUT /api/v1/rooms/batch (auto-enabled)\n```\n\n**Internal tables (no routes):**\n```typescript\n// definitions/features/rooms/room-bookings.ts\nexport const roomBookings = sqliteTable(...);\n// NO default export → No routes generated\n```\n\n**Actions:**\n```typescript\n// definitions/features/rooms/actions.ts\nexport default defineActions(rooms, {\n book: { ... },\n cancel: { ... }\n});\n// → Generates: POST /api/v1/rooms/:id/book, POST /api/v1/rooms/:id/cancel\n```\n\n### 3. Validation\n\nThe compiler validates your configuration at build time, catching errors before deployment. See [Definitions](/compiler/definitions) for details on what's validated.\n\n### 4. Code Generation\n\nThe compiler generates different files based on your provider:\n\n#### Cloudflare Workers (Hono)\n\n```typescript\n// Generated: src/routes/rooms.ts\n\nconst app = new Hono();\n\napp.get('/rooms', async (c) => {\n const ctx = c.get('ctx');\n let query = db.select().from(rooms);\n query = applyFirewall(query, ctx, 'rooms');\n await checkAccess(ctx, 'rooms', 'list');\n const results = await query;\n return c.json(results);\n});\n```\n\n### 5. Type Generation\n\nThe compiler generates TypeScript types for your API:\n\n```typescript\n// Generated: src/types/rooms.ts\nexport type Room = typeof rooms.$inferSelect;\nexport type RoomInsert = typeof rooms.$inferInsert;\n```\n\n## Build-Time vs Runtime\n\n### Build Time (Compilation)\n\nThe compiler validates security configuration, checks for schema/firewall mismatches, generates API route handlers, creates TypeScript types, and prepares migration setup.\n\n### Runtime (API Requests)\n\nWhen your API receives a request: Authentication → Firewall → Access → Guards → Masking → Response. The compiled code handles all of this automatically.\n\n## Development Workflow\n\n1. **Define resources** in `definitions/features/`\n2. **Compile** with `quickback compile`\n3. **Review migrations** in `drizzle/`\n4. **Apply migrations** for your provider\n5. **Test locally** with `npm run dev`\n6. **Deploy** with `npm run deploy`\n\n## Next Steps\n\n- [Getting Started](/compiler/getting-started) — Create your first project\n- [Definitions](/compiler/definitions) — Schema, security layers, and actions\n- [Using the API](/compiler/using-the-api) — CRUD endpoints and filtering\n- [Cloud Compiler](/compiler/cloud-compiler) — CLI and authentication"
222
+ "content": "The Quickback compiler transforms your declarative resource definitions into a complete, production-ready API. It analyzes your TypeScript definitions at build time, generates optimized code, validates your security configuration, and creates database migrations.\n\n## What the Compiler Does\n\nWhen you run `quickback compile`, the compiler:\n\n1. **Reads your definitions** - Analyzes all `defineTable()` configurations in your `quickback/features/` folder\n2. **Validates security** - Checks that firewall, guards, access, and masking are properly configured\n3. **Generates API routes** - Creates REST endpoints for each resource (GET, POST, PATCH, DELETE, plus batch operations)\n4. **Generates actions** - Creates custom endpoints from your `defineActions()` definitions\n5. **Creates middleware** - Generates authentication, authorization, and data validation logic\n6. **Generates TypeScript types** - Creates type-safe interfaces for your API\n7. **Generates migrations** - Automatically runs `drizzle-kit generate` to create database migration files\n\n**Input:**\n```\nquickback/\n├── quickback.config.ts # Compiler configuration\n└── features/\n └── jobs/\n ├── jobs.ts # defineTable(...)\n └── actions.ts # defineActions(...)\n```\n\n**Output:**\n```\nsrc/\n├── routes/\n│ └── jobs.ts # Generated API handlers\n├── middleware/\n│ ├── auth.ts # Authentication logic\n│ └── firewall.ts # Data isolation queries\n└── types/\n └── jobs.ts # TypeScript interfaces\n```\n\n## Basic Usage\n\n### Compile Your Project\n\n```bash\nquickback compile\n```\n\nThis command:\n- Reads your `quickback.config.ts`\n- Analyzes all resource definitions\n- Generates the complete API codebase\n- Reports any configuration errors\n\n### After Compilation\n\nOnce compilation succeeds, you need to:\n\n1. **Apply migrations:**\n\n Run the database migration for your provider. The generated `package.json` includes the appropriate migration script for your setup.\n\n > **Note:** The compiler automatically runs `drizzle-kit generate` during compilation, so you only need to apply the migrations.\n\n2. **Deploy your API:**\n ```bash\n npm run deploy # Cloudflare Workers\n # or\n npm start # Local development\n ```\n\n## The Compilation Process\n\n### 1. Configuration Loading\n\nThe compiler reads your `quickback.config.ts`:\n\n```typescript\n\nexport default defineConfig({\n name: 'my-app',\n template: 'hono',\n features: ['organizations'],\n providers: {\n runtime: defineRuntime('cloudflare'),\n database: defineDatabase('cloudflare-d1'),\n auth: defineAuth('better-auth'),\n },\n});\n```\n\nThis determines:\n- Which application template to use (`hono` or experimental `nextjs`)\n- Which database adapter to use (D1, SQLite, Turso, etc.)\n- Which authentication system to integrate\n- Which runtime to target (Cloudflare Workers, Bun, Node.js)\n- Which auth plugins to enable (based on `features`)\n\n### 2. Resource Discovery\n\nThe compiler scans `quickback/features/` and identifies:\n\n**Resources (with routes):**\n```typescript\n// quickback/features/jobs/jobs.ts\nexport default defineTable(jobs, { ... });\n// → Generates: GET/POST/PATCH/DELETE /api/v1/jobs\n// → Also generates: POST/PATCH/DELETE/PUT /api/v1/jobs/batch (auto-enabled)\n```\n\n**Internal tables (no routes):**\n```typescript\n// quickback/features/jobs/job-applications.ts\nexport const jobApplications = sqliteTable(...);\n// NO default export → No routes generated\n```\n\n**Actions:**\n```typescript\n// quickback/features/jobs/actions.ts\nexport default defineActions(jobs, {\n publish: { ... },\n close: { ... }\n});\n// → Generates: POST /api/v1/jobs/:id/publish, POST /api/v1/jobs/:id/close\n```\n\n### 3. Validation\n\nThe compiler validates your configuration at build time, catching errors before deployment. See [Definitions](/compiler/definitions) for details on what's validated.\n\n### 4. Code Generation\n\nThe compiler generates different files based on your provider:\n\n#### Cloudflare Workers (Hono)\n\n```typescript\n// Generated: src/routes/jobs.ts\n\nconst app = new Hono();\n\napp.get('/jobs', async (c) => {\n const ctx = c.get('ctx');\n let query = db.select().from(jobs);\n query = applyFirewall(query, ctx, 'jobs');\n await checkAccess(ctx, 'jobs', 'list');\n const results = await query;\n return c.json(results);\n});\n```\n\n### 5. Type Generation\n\nThe compiler generates TypeScript types for your API:\n\n```typescript\n// Generated: src/types/jobs.ts\nexport type Job = typeof jobs.$inferSelect;\nexport type JobInsert = typeof jobs.$inferInsert;\n```\n\n## Build-Time vs Runtime\n\n### Build Time (Compilation)\n\nThe compiler validates security configuration, checks for schema/firewall mismatches, generates API route handlers, creates TypeScript types, and prepares migration setup.\n\n### Runtime (API Requests)\n\nWhen your API receives a request: Authentication → Firewall → Access → Guards → Masking → Response. The compiled code handles all of this automatically.\n\n## Development Workflow\n\n1. **Define resources** in `quickback/features/`\n2. **Compile** with `quickback compile`\n3. **Review migrations** in `drizzle/`\n4. **Apply migrations** for your provider\n5. **Test locally** with `npm run dev`\n6. **Deploy** with `npm run deploy`\n\n## Next Steps\n\n- [Getting Started](/compiler/getting-started) — Create your first project\n- [Definitions](/compiler/definitions) — Schema, security layers, and actions\n- [Using the API](/compiler/using-the-api) — CRUD endpoints and filtering\n- [Cloud Compiler](/compiler/cloud-compiler) — CLI and authentication"
215
223
  },
216
224
  "compiler/integrations/cloudflare": {
217
225
  "title": "Cloudflare Workers",
218
- "content": "The Cloudflare target generates a complete Hono-based API running on Cloudflare Workers with D1 as the database.\n\n## Configuration\n\n```typescript\n\nexport default defineConfig({\n name: \"my-app\",\n providers: {\n runtime: defineRuntime(\"cloudflare\"),\n database: defineDatabase(\"cloudflare-d1\"),\n auth: defineAuth(\"better-auth\"),\n },\n});\n```\n\n## Generated Output\n\n```\nsrc/\n├── routes/ # Hono route handlers\n├── middleware/ # Auth, firewall, access middleware\n├── types/ # TypeScript interfaces\n├── db/\n│ └── schema.ts # Drizzle schema\n└── index.ts # Hono app entry point\n\ndrizzle/\n├── migrations/ # SQL migration files\n└── meta/ # Drizzle metadata\n```\n\n## Security Model\n\nAll four security layers run at the application level:\n\n1. **Firewall** — Drizzle WHERE clauses for data isolation\n2. **Access** — Role checks in middleware\n3. **Guards** — Field filtering in request handlers\n4. **Masking** — Response transformation before sending\n\n## Features\n\n- Full CRUD with batch operations\n- Custom actions with inline or handler-based execution\n- Soft delete support\n- Pagination, filtering, and sorting\n- Views (column-level projections)\n- OpenAPI specification generation\n- TypeScript client SDK generation\n\n## Deployment\n\n```bash\n# Development\nnpm run dev\n\n# Production\nnpm run deploy\n# or\nwrangler deploy\n```"
226
+ "content": "The Cloudflare target generates a complete Hono-based API running on Cloudflare Workers with D1 as the database. This is the recommended target for production deployments.\n\n## Configuration\n\n```typescript\n\nexport default defineConfig({\n name: \"my-app\",\n template: \"hono\",\n features: [\"organizations\"],\n providers: {\n runtime: defineRuntime(\"cloudflare\"),\n database: defineDatabase(\"cloudflare-d1\"),\n auth: defineAuth(\"better-auth\"),\n },\n});\n```\n\n## Generated Output\n\n```\nsrc/\n├── routes/ # Hono route handlers (one per resource)\n├── middleware/ # Auth, firewall, access middleware\n├── features/ # Feature-specific helpers (security, masking)\n├── types/ # TypeScript interfaces\n├── db/\n│ └── schema.ts # Drizzle schema\n└── index.ts # Hono app entry point\n\ndrizzle/\n├── migrations/ # SQL migration files\n└── meta/ # Drizzle metadata\n```\n\n## Security Model\n\nAll four security layers run at the application level:\n\n1. **Firewall** — Drizzle WHERE clauses for data isolation\n2. **Access** — Role checks in middleware\n3. **Guards** — Field filtering in request handlers\n4. **Masking** — Response transformation before sending\n\nSince D1 is only accessible through your Worker (no external connection string), the application layer is the only entry point. See [D1 Security Architecture](/stack/database/d1#security-architecture) for details.\n\n## Features\n\n- Full CRUD with batch operations (create, update, delete, upsert)\n- Custom actions with inline or handler-based execution\n- Soft delete with cascading to child tables\n- Pagination, filtering, sorting, field selection, and full-text search\n- Views (column-level projections with per-view access control)\n- OpenAPI specification generation at `/openapi.json`\n\n## Deployment\n\n```bash\n# Local development\nnpm run dev\n\n# Apply migrations to remote D1\nnpm run db:migrate:remote\n\n# Deploy to Cloudflare Workers\nnpm run deploy\n```\n\n## Environment & Bindings\n\nYour `wrangler.toml` needs these bindings:\n\n| Binding | Type | Required | Purpose |\n|---------|------|----------|---------|\n| `DB` | D1 | Yes | Feature database |\n| `AUTH_DB` | D1 | Yes | Better Auth database |\n| `AUDIT_DB` | D1 | No* | Unsafe cross-tenant action audit logs |\n| `KV` | KV | Yes | Session storage |\n| `R2` | R2 | No | File storage (avatars, uploads) |\n| `AI` | Workers AI | No | Embeddings generation |\n| `VECTORIZE` | Vectorize | No | Vector search index |\n| `BROADCASTER` | Service | No | Realtime broadcasts |\n\n\\* Required when you define cross-tenant unsafe actions.\n\n## See Also\n\n- [Cloudflare Template](/compiler/getting-started/template-cloudflare) — Step-by-step setup guide with full wrangler.toml configuration\n- [D1 Database](/stack/database/d1) — D1 setup, multi-database pattern, and security architecture\n- [Neon Integration](/compiler/integrations/neon) — Alternative: PostgreSQL with RLS\n- [Supabase Integration](/compiler/integrations/supabase) — Alternative: Supabase with RLS"
219
227
  },
220
228
  "compiler/integrations": {
221
229
  "title": "Compile Targets",
@@ -229,41 +237,45 @@ export const DOCS = {
229
237
  "title": "Supabase",
230
238
  "content": "Quickback supports Supabase as a deployment target, generating PostgreSQL schemas with Row Level Security (RLS) policies that enforce your firewall configuration at the database level.\n\n## Why Supabase?\n\n- **Full PostgreSQL** — Advanced queries, joins, PostGIS, full-text search\n- **Database-Level Security** — RLS policies enforce access even if API is bypassed\n- **Integrated Auth** — Supabase Auth with JWT claims for RLS\n- **Real-time** — Postgres Changes for live updates\n- **Managed Infrastructure** — Automatic backups, scaling, monitoring\n\n## Configuration\n\n```typescript\n\nexport default defineConfig({\n name: \"my-app\",\n providers: {\n runtime: defineRuntime(\"supabase\"),\n database: defineDatabase(\"supabase\"),\n auth: defineAuth(\"supabase-auth\"),\n },\n});\n```\n\n## Key Difference from Neon\n\n| Feature | Supabase | Neon |\n|---------|----------|------|\n| Auth function | `auth.uid()` | `auth.user_id()` |\n| Auth provider | Supabase Auth | Neon Authorize + Better Auth |\n| Users table | `auth.users` | `public.users` (Better Auth) |\n\n## Row Level Security\n\nQuickback generates RLS policies from your firewall config:\n\n```typescript\nfirewall: {\n organization: {},\n owner: { mode: 'optional' }\n}\n```\n\n```sql\n-- Generated policy\nCREATE POLICY \"documents_select\" ON documents FOR SELECT\nUSING (\n organization_id = get_active_org_id()\n AND (has_any_role(ARRAY['admin']) OR user_id = auth.uid())\n);\n```\n\n### Firewall Patterns\n\n**Organization-Scoped:**\n```sql\nCREATE POLICY \"projects_select\" ON projects FOR SELECT\nUSING (organization_id = public.get_active_org_id());\n```\n\n**User-Scoped:**\n```sql\nCREATE POLICY \"preferences_select\" ON preferences FOR SELECT\nUSING (user_id = auth.uid());\n```\n\n## Defense in Depth\n\nAll generated policies include a deny policy for anonymous users:\n\n```sql\nCREATE POLICY \"tablename_deny_anon\" ON tablename FOR ALL\nTO anon\nUSING (false);\n```\n\n## Generated Helper Functions\n\n| Function | Purpose |\n|----------|---------|\n| `get_active_org_id()` | Returns user's active organization from `user_sessions` |\n| `has_any_role(roles[])` | Checks if user has any specified role |\n| `has_org_role(role)` | Checks for a specific role |\n| `is_org_member()` | Checks org membership |\n| `get_user_role()` | Returns user's role in active org |\n\n## Generated Files\n\n```\nsupabase/\n├── migrations/\n│ ├── 0100_create_rls_helpers.sql\n│ ├── 0101_create_rls_policies.sql\n│ └── 0102_create_audit_triggers.sql\nsrc/\n├── db/\n│ └── schema.ts\n└── lib/\n └── supabase.ts\n```\n\n## Getting Started\n\n1. Install the Supabase CLI\n2. Configure your Quickback project for Supabase\n3. Run `quickback compile`\n4. Push migrations with `supabase db push`"
231
239
  },
240
+ "compiler/troubleshooting": {
241
+ "title": "Troubleshooting",
242
+ "content": "Common issues and their solutions. For cloud compiler specific issues, see [Cloud Compiler Troubleshooting](/compiler/cloud-compiler/troubleshooting).\n\n## Compilation Issues\n\n### \"Feature must use defineTable()\"\n\nYour table file is missing the default export:\n\n```typescript\n// Wrong — no default export\nexport const applications = sqliteTable(\"applications\", { ... });\n\n// Correct — add defineTable default export\nexport const applications = sqliteTable(\"applications\", { ... });\nexport default defineTable(applications, { ... });\n```\n\n### \"File exports multiple tables with defineTable\"\n\nA single file can export multiple Drizzle tables, but only ONE can have a `defineTable()` default export. Split tables into separate files:\n\n```\nquickback/features/applications/\n├── applications.ts # defineTable(applications, { ... })\n└── candidates.ts # defineTable(candidates, { ... })\n```\n\n### Missing fields in generated API\n\nIf a field exists in your schema but doesn't appear in API responses:\n\n1. **Guards:** Check that the field is in `createable` (for POST) or `updatable` (for PATCH). Fields not listed are silently stripped.\n2. **Views:** If using views, the field must be in the view's `fields` array.\n3. **Masking:** The field may be present but masked (showing `[REDACTED]` or `***`). Check your masking config.\n\n### \"organizationId\" not auto-injected\n\nThe firewall auto-detects `organizationId` and `ownerId` columns by name. If your column uses a different name, you need to configure the firewall explicitly:\n\n```typescript\nfirewall: {\n organization: { column: \"org_id\" }, // Custom column name\n}\n```\n\n## Migration Issues\n\n### Columns persist after removal from schema\n\nQuickback (via Drizzle) does **not** generate DROP COLUMN migrations. If you remove a column from your schema, the database column will remain as an orphaned column. SQLite handles this gracefully — the column won't appear in API responses since Drizzle only selects defined columns.\n\nTo clean up, you'd need to manually create a migration.\n\n### Drizzle rename prompts in CI\n\nIf compilation fails with `drizzle-kit requested interactive rename input`, add explicit rename hints in your config. See [Drizzle rename prompts](/compiler/cloud-compiler/troubleshooting#drizzle-rename-prompts-in-ciheadless-compile).\n\n## Runtime Issues\n\n### 401 on all requests\n\n1. **Missing auth:** Ensure your request includes `Authorization: Bearer <token>` or the session cookie\n2. **Expired session:** Sessions last 7 days by default. Re-authenticate.\n3. **Wrong auth URL:** Check that `BETTER_AUTH_URL` matches your deployment URL\n\n### 403 Forbidden\n\n1. **Wrong role:** Check the `access` config for the endpoint — your role may not be listed\n2. **Wrong org:** You may be in a different organization than the record's `organizationId`\n3. **Firewall hiding:** If `errorMode: 'hide'` is set, firewall violations return 404 instead of 403\n\n### 400 Bad Request on create/update\n\n1. **Guard violation:** You're sending a field not in `createable` or `updatable`. Check the error's `details.fields`.\n2. **Missing required field:** A `.notNull()` column without a default is missing from your request body.\n3. **Immutable field:** You're trying to update a field marked as `immutable`.\n\n### Records missing from list\n\nRecords may be filtered by:\n1. **Firewall:** Only records matching your organization (and optionally owner) are returned\n2. **Soft delete:** Soft-deleted records are automatically excluded\n3. **Access conditions:** Record-level conditions may filter results\n\n## See Also\n\n- [Cloud Compiler Troubleshooting](/compiler/cloud-compiler/troubleshooting) — CLI and authentication issues\n- [Errors](/compiler/using-the-api/errors) — Complete error code reference\n- [Firewall](/compiler/definitions/firewall) — Data isolation configuration\n- [Guards](/compiler/definitions/guards) — Field modification rules"
243
+ },
232
244
  "compiler/using-the-api/actions-api": {
233
245
  "title": "Actions API",
234
- "content": "Actions provide custom endpoints beyond CRUD operations. They support input validation, access conditions, and multiple response types including streaming.\n\n## Record-Based Actions\n\nRecord-based actions operate on a specific record identified by ID.\n\n```\nPOST /api/v1/{resource}/:id/{action-name}\n```\n\n### Request\n\n```bash\n# With JSON body\ncurl -X POST /api/v1/orders/ord_123/refund \\\n -H \"Authorization: Bearer <token>\" \\\n -H \"Content-Type: application/json\" \\\n -d '{ \"reason\": \"Customer request\", \"amount\": 50.00 }'\n```\n\nThe action's input schema validates the request body using Zod. Invalid input returns a 400 error.\n\n### Response\n\n```json\n{\n \"success\": true,\n \"data\": {\n \"refundId\": \"ref_456\",\n \"amount\": 50.00,\n \"status\": \"processed\"\n }\n}\n```\n\n### Security Flow\n\n1. **Authentication** — User must be logged in (401 if not)\n2. **Firewall** — Record must exist and be accessible to the user (404 if not found or not owned)\n3. **Access** — User must have the required role (403 if insufficient)\n4. **Input validation** — Request body validated against the action's Zod schema (400 if invalid)\n5. **Access conditions** — Record must match access conditions (400 if not met)\n6. **Masking** — Applied to the response data\n\n### Access Conditions\n\nActions can require the record to be in a specific state:\n\n```typescript\n// In actions.ts\ncomplete: {\n access: {\n roles: [\"owner\", \"admin\", \"member\"],\n record: { completed: { equals: false } },\n },\n}\n```\n\nIf the record doesn't match (`completed` is already `true`), the action returns 400.\n\n## Standalone Actions\n\nStandalone actions don't require a record ID. They're for operations that aren't tied to a specific record.\n\n```\nPOST /api/v1/{resource}/{action-name}\n```\n\n### Request\n\n```bash\ncurl -X POST /api/v1/reports/generate-summary \\\n -H \"Authorization: Bearer <token>\" \\\n -H \"Content-Type: application/json\" \\\n -d '{ \"startDate\": \"2025-01-01\", \"endDate\": \"2025-01-31\" }'\n```\n\n### Response\n\nSame format as record-based actions. The difference is no record lookup or firewall check.\n\n### Security Flow\n\n1. **Authentication** — Required (401)\n2. **Organization check** — For org-scoped resources, user must have an active organization (403)\n3. **Access** — Role-based check (403)\n4. **Scoped DB** — The `db` passed to the handler is auto-scoped based on table columns (org isolation, owner filtering, soft delete)\n5. **Input validation** — Zod schema validation (400)\n\n### Scoped Database\n\nAll actions (both standalone and record-based) receive a **scoped `db`** instance that automatically enforces security based on the table's columns:\n\n- Tables with `organizationId` — org isolation (`WHERE organizationId = ?`)\n- Tables with `ownerId` — owner isolation (`WHERE ownerId = ?`)\n- Tables with `deletedAt` — soft delete filter (`WHERE deletedAt IS NULL`)\n- `INSERT` operations auto-inject `organizationId` and `ownerId` from context\n\n```typescript\n// Your handler receives a scoped db — no manual filtering needed\nexecute: async ({ db, ctx, input }) => {\n // This query automatically includes WHERE organizationId = ? AND ownerId = ? AND deletedAt IS NULL\n const items = await db.select().from(claims).where(eq(claims.status, 'active'));\n\n // Inserts automatically include organizationId and ownerId\n await db.insert(claims).values({ title: input.title });\n\n return items;\n}\n```\n\nIf you need unscoped access (e.g., cross-org admin operations), declare `unsafe: true` on the action:\n\n```typescript\nadminReport: {\n unsafe: true,\n execute: async ({ db, rawDb, ctx, input }) => {\n // rawDb bypasses all security — only available with unsafe: true\n const allOrgs = await rawDb.select().from(invoices);\n return allOrgs;\n },\n}\n```\n\nWithout `unsafe: true`, `rawDb` is `undefined`.\n\n## Input Validation\n\nActions use Zod schemas for input validation. The schema is defined in your `actions.ts` file:\n\n```typescript\n\nexport default defineActions(\"orders\", {\n refund: {\n type: \"record\",\n input: z.object({\n reason: z.string().min(1),\n amount: z.number().positive(),\n }),\n access: {\n roles: [\"admin\"],\n record: { status: { equals: \"completed\" } },\n },\n },\n});\n```\n\nWhen validation fails:\n\n```json\n{\n \"error\": \"Validation failed\",\n \"layer\": \"guards\",\n \"code\": \"GUARD_VALIDATION_FAILED\",\n \"details\": {\n \"issues\": [\n { \"path\": [\"amount\"], \"message\": \"Number must be greater than 0\" }\n ]\n }\n}\n```\n\n## Response Types\n\nActions support three response types:\n\n### JSON (Default)\n\nReturns a JSON object wrapped in `{ success: true, data: ... }`. Masking is applied to the data.\n\n### Streaming\n\nFor long-running operations, actions can return a streaming response:\n\n```typescript\ngenerateReport: {\n type: \"standalone\",\n responseType: \"stream\",\n // ...\n}\n```\n\nThe action handler returns a `ReadableStream` directly — no JSON wrapper.\n\n### File\n\nFor file downloads, actions can return a raw `Response` object:\n\n```typescript\nexportCsv: {\n type: \"record\",\n responseType: \"file\",\n // ...\n}\n```\n\n## Action Handler\n\nThe compiler generates a handler skeleton. Your action receives:\n\n```typescript\n{\n db, // Scoped database (auto-enforces org/owner/soft-delete filters)\n rawDb, // Raw database (only available when unsafe: true, otherwise undefined)\n ctx, // Auth context (user, session, org)\n record, // The existing record (record-based only, undefined for standalone)\n input, // Validated input from Zod schema\n services, // Injected services\n c, // Hono context\n auditFields, // { createdAt, modifiedAt } timestamps\n}\n```\n\n## HTTP Methods\n\nActions default to `POST` but can use any HTTP method:\n\n```typescript\ngetStatus: {\n type: \"record\",\n method: \"GET\",\n // GET actions receive input from query parameters instead of body\n}\n```\n\nWhen using `GET`, input is parsed from query parameters instead of the request body.\n\n## Cascading Soft Delete\n\nWhen soft-deleting a parent record, Quickback automatically soft-deletes related records in child/junction tables **within the same feature**. The compiler detects foreign key references at build time.\n\nFor example, if your feature has `projects` and `projectMembers` tables where `projectMembers` has a `.references(() => projects.id)`, deleting a project will also soft-delete all its members:\n\n```\nDELETE /api/v1/projects/proj_123\n```\n\nGenerated code:\n```typescript\n// Soft delete parent\nawait db.update(projects).set({ deletedAt: now, modifiedAt: now })\n .where(and(buildFirewallConditions(ctx), eq(projects.id, id)));\n\n// Cascade soft delete to child tables\nawait db.update(projectMembers).set({ deletedAt: now, modifiedAt: now })\n .where(eq(projectMembers.projectId, id));\n```\n\n**Cascade rules:**\n- **Soft delete (default)**: Application-level cascade via generated UPDATE statements\n- **Hard delete (`mode: 'hard'`)**: No compiler cascade — relies on DB-level `ON DELETE CASCADE`\n- **Cross-feature**: No cascade — only within the same feature's tables\n\n## See Also\n\n- [Defining Actions](/compiler/definitions/actions) — How to define actions in your feature\n- [Guards](/compiler/definitions/guards) — Guard conditions reference\n- [Error Responses](/compiler/using-the-api/errors) — Error format reference"
246
+ "content": "Actions provide custom endpoints beyond CRUD operations. They support input validation, access conditions, and multiple response types including streaming.\n\n## Record-Based Actions\n\nRecord-based actions operate on a specific record identified by ID.\n\n```\nPOST /api/v1/{resource}/:id/{action-name}\n```\n\n### Request\n\n```bash\n# With JSON body\ncurl -X POST /api/v1/applications/app_123/advance-stage \\\n -H \"Authorization: Bearer <token>\" \\\n -H \"Content-Type: application/json\" \\\n -d '{ \"notes\": \"Strong technical interview, moving to offer stage\" }'\n```\n\nThe action's input schema validates the request body using Zod. Invalid input returns a 400 error.\n\n### Response\n\n```json\n{\n \"success\": true,\n \"data\": {\n \"id\": \"app_123\",\n \"stage\": \"offer\",\n \"previousStage\": \"interview\",\n \"notes\": \"Strong technical interview, moving to offer stage\"\n }\n}\n```\n\n### Security Flow\n\n1. **Authentication** — User must be logged in (401 if not)\n2. **Firewall** — Record must exist and be accessible to the user (404 if not found or not owned)\n3. **Access** — User must have the required role (403 if insufficient)\n4. **Input validation** — Request body validated against the action's Zod schema (400 if invalid)\n5. **Access conditions** — Record must match access conditions (400 if not met)\n6. **Masking** — Applied to the response data\n\n### Access Conditions\n\nActions can require the record to be in a specific state:\n\n```typescript\n// In actions.ts\nreject: {\n access: {\n roles: [\"owner\", \"hiring-manager\", \"recruiter\"],\n record: { stage: { notEquals: \"rejected\" } },\n },\n}\n```\n\nIf the record doesn't match (`stage` is already `rejected`), the action returns 400.\n\n## Standalone Actions\n\nStandalone actions don't require a record ID. They're for operations that aren't tied to a specific record.\n\n```\nPOST /api/v1/{resource}/{action-name}\n```\n\n### Request\n\n```bash\ncurl -X POST /api/v1/candidates/bulk-import \\\n -H \"Authorization: Bearer <token>\" \\\n -H \"Content-Type: application/json\" \\\n -d '{ \"source\": \"LinkedIn\", \"candidates\": [{\"name\": \"Alice Johnson\", \"email\": \"alice@example.com\"}, {\"name\": \"Bob Martinez\", \"email\": \"bob@example.com\"}] }'\n```\n\n### Response\n\nSame format as record-based actions. The difference is no record lookup or firewall check.\n\n### Security Flow\n\n1. **Authentication** — Required (401)\n2. **Organization check** — For org-scoped resources, user must have an active organization (403)\n3. **Access** — Role-based check (403)\n4. **Scoped DB** — The `db` passed to the handler is auto-scoped based on table columns (org isolation, owner filtering, soft delete)\n5. **Input validation** — Zod schema validation (400)\n\n### Scoped Database\n\nAll actions (both standalone and record-based) receive a **scoped `db`** instance that automatically enforces security based on the table's columns:\n\n- Tables with `organizationId` — org isolation (`WHERE organizationId = ?`)\n- Tables with `ownerId` — owner isolation (`WHERE ownerId = ?`)\n- Tables with `deletedAt` — soft delete filter (`WHERE deletedAt IS NULL`)\n- `INSERT` operations auto-inject `organizationId` and `ownerId` from context\n\n```typescript\n// Your handler receives a scoped db — no manual filtering needed\nexecute: async ({ db, ctx, input }) => {\n // This query automatically includes WHERE organizationId = ? AND ownerId = ? AND deletedAt IS NULL\n const items = await db.select().from(applications).where(eq(applications.stage, 'interview'));\n\n // Inserts automatically include organizationId and ownerId\n await db.insert(applications).values({ candidateId: input.candidateId, jobId: input.jobId, stage: 'applied' });\n\n return items;\n}\n```\n\nIf you need unscoped access (for example, audited platform admin operations), declare `unsafe` on the action:\n\n```typescript\nadminReport: {\n unsafe: {\n reason: 'Platform support report',\n adminOnly: true,\n crossTenant: true,\n targetScope: 'all',\n },\n execute: async ({ db, rawDb, ctx, input }) => {\n // rawDb bypasses scoped filters\n const allOrgs = await rawDb.select().from(applications);\n return allOrgs;\n },\n}\n```\n\nCross-tenant unsafe actions are generated with:\n\n- Better Auth authentication required\n- platform admin gate (`ctx.userRole === 'admin'`)\n- mandatory audit logging on denial/success/error\n\nWithout unsafe mode, `rawDb` is `undefined`.\n\nRaw SQL in action code is blocked by default at compile time. If you need it for a specific action, set `allowRawSql: true` on that action explicitly.\n\n## Input Validation\n\nActions use Zod schemas for input validation. The schema is defined in your `actions.ts` file:\n\n```typescript\n\nexport default defineActions(\"applications\", {\n \"advance-stage\": {\n type: \"record\",\n input: z.object({\n notes: z.string().optional(),\n }),\n access: {\n roles: [\"hiring-manager\", \"recruiter\"],\n record: { stage: { notEquals: \"hired\" } },\n },\n },\n reject: {\n type: \"record\",\n input: z.object({\n reason: z.string().min(1),\n }),\n access: {\n roles: [\"hiring-manager\", \"recruiter\"],\n record: { stage: { notEquals: \"rejected\" } },\n },\n },\n});\n```\n\nWhen validation fails:\n\n```json\n{\n \"error\": \"Validation failed\",\n \"layer\": \"guards\",\n \"code\": \"GUARD_VALIDATION_FAILED\",\n \"details\": {\n \"issues\": [\n { \"path\": [\"reason\"], \"message\": \"String must contain at least 1 character(s)\" }\n ]\n }\n}\n```\n\n## Response Types\n\nActions support three response types:\n\n### JSON (Default)\n\nReturns a JSON object wrapped in `{ success: true, data: ... }`. Masking is applied to the data.\n\n### Streaming\n\nFor long-running operations, actions can return a streaming response:\n\n```typescript\ngenerateReport: {\n type: \"standalone\",\n responseType: \"stream\",\n // ...\n}\n```\n\nThe action handler returns a `ReadableStream` directly — no JSON wrapper.\n\n### File\n\nFor file downloads, actions can return a raw `Response` object:\n\n```typescript\nexportCsv: {\n type: \"record\",\n responseType: \"file\",\n // ...\n}\n```\n\n## Action Handler\n\nThe compiler generates a handler skeleton. Your action receives:\n\n```typescript\n{\n db, // Scoped database (auto-enforces org/owner/soft-delete filters)\n rawDb, // Raw database (only available when unsafe mode is enabled)\n ctx, // Auth context (user, session, org)\n record, // The existing record (record-based only, undefined for standalone)\n input, // Validated input from Zod schema\n services, // Injected services\n c, // Hono context\n auditFields, // { createdAt, modifiedAt } timestamps\n}\n```\n\n## HTTP Methods\n\nActions default to `POST` but can use any HTTP method:\n\n```typescript\ngetStatus: {\n type: \"record\",\n method: \"GET\",\n // GET actions receive input from query parameters instead of body\n}\n```\n\nWhen using `GET`, input is parsed from query parameters instead of the request body.\n\n## Cascading Soft Delete\n\nWhen soft-deleting a parent record, Quickback automatically soft-deletes related records in child/junction tables **within the same feature**. The compiler detects foreign key references at build time.\n\nFor example, if your feature has `jobs` and `applications` tables where `applications` has a `.references(() => jobs.id)`, deleting a job will also soft-delete all its applications:\n\n```\nDELETE /api/v1/jobs/job_123\n```\n\nGenerated code:\n```typescript\n// Soft delete parent\nawait db.update(jobs).set({ deletedAt: now, modifiedAt: now })\n .where(and(buildFirewallConditions(ctx), eq(jobs.id, id)));\n\n// Cascade soft delete to child tables\nawait db.update(applications).set({ deletedAt: now, modifiedAt: now })\n .where(eq(applications.jobId, id));\n```\n\n**Cascade rules:**\n- **Soft delete (default)**: Application-level cascade via generated UPDATE statements\n- **Hard delete (`mode: 'hard'`)**: No compiler cascade — relies on DB-level `ON DELETE CASCADE`\n- **Cross-feature**: No cascade — only within the same feature's tables\n\n## See Also\n\n- [Defining Actions](/compiler/definitions/actions) — How to define actions in your feature\n- [Guards](/compiler/definitions/guards) — Guard conditions reference\n- [Error Responses](/compiler/using-the-api/errors) — Error format reference"
235
247
  },
236
248
  "compiler/using-the-api/batch-operations": {
237
249
  "title": "Batch Operations",
238
- "content": "Batch operations let you create, update, or delete multiple records in a single request. They are **auto-enabled** when the corresponding CRUD operation exists in your definition.\n\n## Available Endpoints\n\n| Endpoint | Method | Auto-enabled When |\n|----------|--------|-------------------|\n| `/{resource}/batch` | `POST` | `crud.create` exists |\n| `/{resource}/batch` | `PATCH` | `crud.update` exists |\n| `/{resource}/batch` | `DELETE` | `crud.delete` exists |\n| `/{resource}/batch` | `PUT` | `crud.put` exists |\n\nTo disable a batch operation, set it to `false` in your definition:\n\n```typescript\ncrud: {\n create: { access: { roles: ['admin'] } },\n batchCreate: false, // Disable batch create\n}\n```\n\n## Batch Create\n\n```\nPOST /api/v1/{resource}/batch\n```\n\n### Request\n\n```json\n{\n \"records\": [\n { \"name\": \"Room A\", \"capacity\": 10 },\n { \"name\": \"Room B\", \"capacity\": 20 },\n { \"name\": \"Room C\", \"capacity\": 30 }\n ],\n \"options\": {\n \"atomic\": false\n }\n}\n```\n\n### Response (201 or 207)\n\n```json\n{\n \"success\": [\n { \"id\": \"rm_abc1\", \"name\": \"Room A\", \"capacity\": 10, \"createdAt\": \"2025-01-15T10:00:00Z\" },\n { \"id\": \"rm_abc3\", \"name\": \"Room C\", \"capacity\": 30, \"createdAt\": \"2025-01-15T10:00:00Z\" }\n ],\n \"errors\": [\n {\n \"index\": 1,\n \"record\": { \"name\": \"Room B\", \"capacity\": 20 },\n \"error\": {\n \"error\": \"Database insert failed\",\n \"layer\": \"validation\",\n \"code\": \"INSERT_FAILED\",\n \"details\": { \"reason\": \"UNIQUE constraint failed: rooms.name\" }\n }\n }\n ],\n \"meta\": {\n \"total\": 3,\n \"succeeded\": 2,\n \"failed\": 1,\n \"atomic\": false\n }\n}\n```\n\n- **201** — All records created successfully\n- **207** — Partial success (some errors, some successes)\n\nFields applied automatically to each record:\n- ID generation (UUID, prefixed, etc.)\n- Ownership fields (`organizationId`, `ownerId`, `createdBy`)\n- Audit fields (`createdAt`, `modifiedAt`)\n- Default values and computed fields\n\n## Batch Update\n\n```\nPATCH /api/v1/{resource}/batch\n```\n\nEvery record **must include an `id` field**.\n\n### Request\n\n```json\n{\n \"records\": [\n { \"id\": \"rm_abc1\", \"capacity\": 15 },\n { \"id\": \"rm_abc2\", \"name\": \"Updated Room\" },\n { \"id\": \"rm_xyz9\", \"capacity\": 50 }\n ],\n \"options\": {\n \"atomic\": false\n }\n}\n```\n\n### Response (200 or 207)\n\n```json\n{\n \"success\": [\n { \"id\": \"rm_abc1\", \"name\": \"Room A\", \"capacity\": 15, \"modifiedAt\": \"2025-01-15T11:00:00Z\" },\n { \"id\": \"rm_abc2\", \"name\": \"Updated Room\", \"capacity\": 20, \"modifiedAt\": \"2025-01-15T11:00:00Z\" }\n ],\n \"errors\": [\n {\n \"index\": 2,\n \"record\": { \"id\": \"rm_xyz9\", \"capacity\": 50 },\n \"error\": {\n \"error\": \"Not found\",\n \"layer\": \"firewall\",\n \"code\": \"NOT_FOUND\",\n \"details\": { \"id\": \"rm_xyz9\" }\n }\n }\n ],\n \"meta\": {\n \"total\": 3,\n \"succeeded\": 2,\n \"failed\": 1,\n \"atomic\": false\n }\n}\n```\n\nRecords that don't exist or aren't accessible through the firewall return a `NOT_FOUND` error. Guard rules (immutable, protected, not-updatable fields) are checked per record.\n\n### Missing IDs\n\nIf any records are missing the `id` field, the entire request is rejected:\n\n```json\n{\n \"error\": \"Records missing required ID field\",\n \"layer\": \"validation\",\n \"code\": \"BATCH_MISSING_IDS\",\n \"details\": { \"indices\": [0, 2] },\n \"hint\": \"All records must include an ID field for batch update.\"\n}\n```\n\n## Batch Delete\n\n```\nDELETE /api/v1/{resource}/batch\n```\n\n### Request\n\n```json\n{\n \"ids\": [\"rm_abc1\", \"rm_abc2\", \"rm_abc3\"],\n \"options\": {\n \"atomic\": false\n }\n}\n```\n\nNote: Batch delete uses an `ids` array (not `records`).\n\n### Response (200 or 207)\n\n**Soft delete** (default):\n\n```json\n{\n \"success\": [\n { \"id\": \"rm_abc1\", \"name\": \"Room A\", \"deletedAt\": \"2025-01-15T12:00:00Z\" },\n { \"id\": \"rm_abc2\", \"name\": \"Room B\", \"deletedAt\": \"2025-01-15T12:00:00Z\" }\n ],\n \"errors\": [],\n \"meta\": {\n \"total\": 2,\n \"succeeded\": 2,\n \"failed\": 0,\n \"atomic\": false\n }\n}\n```\n\n**Hard delete**: Returns objects with only the `id` field (record data is deleted).\n\n## Batch Upsert\n\n```\nPUT /api/v1/{resource}/batch\n```\n\nInserts new records or updates existing ones. Every record must include an `id` field.\n\n**Note:** Batch upsert is only available when `generateId` is set to `false` (user-provided IDs) in your configuration.\n\n### Request\n\n```json\n{\n \"records\": [\n { \"id\": \"rm_001\", \"name\": \"Room A\", \"capacity\": 10 },\n { \"id\": \"rm_002\", \"name\": \"Updated Room B\", \"capacity\": 25 }\n ],\n \"options\": {\n \"atomic\": false\n }\n}\n```\n\n### Response (201 or 207)\n\nThe compiler checks which IDs already exist and splits the batch into creates and updates. Create records get ownership and default fields; update records get only `modifiedAt`.\n\n## Atomic Mode\n\nBy default, batch operations use **partial success** mode — each record is processed independently, and failures don't affect other records.\n\nSet `\"atomic\": true` to require all-or-nothing processing:\n\n```json\n{\n \"records\": [...],\n \"options\": {\n \"atomic\": true\n }\n}\n```\n\n### Atomic Failure Response (400)\n\nIf any record fails in atomic mode, the entire batch is rejected:\n\n```json\n{\n \"error\": \"Batch operation failed in atomic mode\",\n \"layer\": \"validation\",\n \"code\": \"BATCH_ATOMIC_FAILED\",\n \"details\": {\n \"failedAt\": 1,\n \"reason\": \"Database insert failed\",\n \"errorDetails\": { \"reason\": \"UNIQUE constraint failed\" }\n },\n \"hint\": \"Transaction rolled back. Fix the error and retry the entire batch.\"\n}\n```\n\n## Batch Size Limit\n\nThe default maximum batch size is **100 records**. Requests exceeding the limit are rejected:\n\n```json\n{\n \"error\": \"Batch size limit exceeded\",\n \"layer\": \"validation\",\n \"code\": \"BATCH_SIZE_EXCEEDED\",\n \"details\": { \"max\": 100, \"actual\": 250 },\n \"hint\": \"Maximum 100 records allowed per batch. Split into multiple requests.\"\n}\n```\n\n## Configuration\n\nBatch operations inherit access control from their corresponding CRUD operation. You can override per-batch settings:\n\n```typescript\nexport default defineTable(rooms, {\n crud: {\n create: { access: { roles: ['admin', 'member'] } },\n update: { access: { roles: ['admin', 'member'] } },\n delete: { access: { roles: ['admin'] }, mode: 'soft' },\n\n // Override batch-specific settings\n batchCreate: {\n access: { roles: ['admin'] }, // More restrictive than single create\n maxBatchSize: 50, // Default: 100\n allowAtomic: true, // Default: true\n },\n batchUpdate: {\n maxBatchSize: 100,\n allowAtomic: true,\n },\n batchDelete: {\n access: { roles: ['admin'] },\n maxBatchSize: 100,\n allowAtomic: true,\n },\n\n // Disable a batch operation entirely\n batchUpsert: false,\n },\n});\n```\n\n## Security\n\nAll security pillars apply to batch operations:\n\n1. **Authentication** — Required for all batch endpoints (401 if missing)\n2. **Firewall** — Applied per-record for update/delete/upsert (not applicable to create)\n3. **Access** — Role-based check using the batch operation's access config\n4. **Guards** — Per-record validation (createable/updatable/immutable fields)\n5. **Masking** — Applied to all records in the success array before response\n\n## See Also\n\n- [CRUD Endpoints](/compiler/using-the-api/crud) — Single-record operations\n- [Error Responses](/compiler/using-the-api/errors) — Error format reference\n- [Guards](/compiler/definitions/guards) — Field-level write protection"
250
+ "content": "Batch operations let you create, update, or delete multiple records in a single request. They are **auto-enabled** when the corresponding CRUD operation exists in your definition.\n\n## Available Endpoints\n\n| Endpoint | Method | Auto-enabled When |\n|----------|--------|-------------------|\n| `/{resource}/batch` | `POST` | `crud.create` exists |\n| `/{resource}/batch` | `PATCH` | `crud.update` exists |\n| `/{resource}/batch` | `DELETE` | `crud.delete` exists |\n| `/{resource}/batch` | `PUT` | `crud.put` exists |\n\nTo disable a batch operation, set it to `false` in your definition:\n\n```typescript\ncrud: {\n create: { access: { roles: ['hiring-manager'] } },\n batchCreate: false, // Disable batch create\n}\n```\n\n## Batch Create\n\n```\nPOST /api/v1/{resource}/batch\n```\n\n### Request\n\n```json\n{\n \"records\": [\n { \"candidateId\": \"cand_101\", \"jobId\": \"job_201\", \"stage\": \"applied\", \"notes\": \"Strong frontend background\" },\n { \"candidateId\": \"cand_101\", \"jobId\": \"job_202\", \"stage\": \"applied\", \"notes\": \"Also interested in backend role\" },\n { \"candidateId\": \"cand_102\", \"jobId\": \"job_201\", \"stage\": \"applied\", \"notes\": \"Referred by employee\" }\n ],\n \"options\": {\n \"atomic\": false\n }\n}\n```\n\n### Response (201 or 207)\n\n```json\n{\n \"success\": [\n { \"id\": \"app_abc1\", \"candidateId\": \"cand_101\", \"jobId\": \"job_201\", \"stage\": \"applied\", \"createdAt\": \"2025-01-15T10:00:00Z\" },\n { \"id\": \"app_abc3\", \"candidateId\": \"cand_102\", \"jobId\": \"job_201\", \"stage\": \"applied\", \"createdAt\": \"2025-01-15T10:00:00Z\" }\n ],\n \"errors\": [\n {\n \"index\": 1,\n \"record\": { \"candidateId\": \"cand_101\", \"jobId\": \"job_202\", \"stage\": \"applied\", \"notes\": \"Also interested in backend role\" },\n \"error\": {\n \"error\": \"Database insert failed\",\n \"layer\": \"validation\",\n \"code\": \"INSERT_FAILED\",\n \"details\": { \"reason\": \"UNIQUE constraint failed: applications.candidateId, applications.jobId\" }\n }\n }\n ],\n \"meta\": {\n \"total\": 3,\n \"succeeded\": 2,\n \"failed\": 1,\n \"atomic\": false\n }\n}\n```\n\n- **201** — All records created successfully\n- **207** — Partial success (some errors, some successes)\n\nFields applied automatically to each record:\n- ID generation (UUID, prefixed, etc.)\n- Ownership fields (`organizationId`, `ownerId`, `createdBy`)\n- Audit fields (`createdAt`, `modifiedAt`)\n- Default values and computed fields\n\n## Batch Update\n\n```\nPATCH /api/v1/{resource}/batch\n```\n\nEvery record **must include an `id` field**.\n\n### Request\n\n```json\n{\n \"records\": [\n { \"id\": \"app_abc1\", \"notes\": \"Passed phone screen, schedule onsite\" },\n { \"id\": \"app_abc2\", \"notes\": \"Moved to technical interview round\" },\n { \"id\": \"app_xyz9\", \"notes\": \"Hiring manager approved offer\" }\n ],\n \"options\": {\n \"atomic\": false\n }\n}\n```\n\n### Response (200 or 207)\n\n```json\n{\n \"success\": [\n { \"id\": \"app_abc1\", \"notes\": \"Passed phone screen, schedule onsite\", \"modifiedAt\": \"2025-01-15T11:00:00Z\" },\n { \"id\": \"app_abc2\", \"notes\": \"Moved to technical interview round\", \"modifiedAt\": \"2025-01-15T11:00:00Z\" }\n ],\n \"errors\": [\n {\n \"index\": 2,\n \"record\": { \"id\": \"app_xyz9\", \"notes\": \"Hiring manager approved offer\" },\n \"error\": {\n \"error\": \"Not found\",\n \"layer\": \"firewall\",\n \"code\": \"NOT_FOUND\",\n \"details\": { \"id\": \"app_xyz9\" }\n }\n }\n ],\n \"meta\": {\n \"total\": 3,\n \"succeeded\": 2,\n \"failed\": 1,\n \"atomic\": false\n }\n}\n```\n\nRecords that don't exist or aren't accessible through the firewall return a `NOT_FOUND` error. Guard rules (immutable, protected, not-updatable fields) are checked per record.\n\n### Missing IDs\n\nIf any records are missing the `id` field, the entire request is rejected:\n\n```json\n{\n \"error\": \"Records missing required ID field\",\n \"layer\": \"validation\",\n \"code\": \"BATCH_MISSING_IDS\",\n \"details\": { \"indices\": [0, 2] },\n \"hint\": \"All records must include an ID field for batch update.\"\n}\n```\n\n## Batch Delete\n\n```\nDELETE /api/v1/{resource}/batch\n```\n\n### Request\n\n```json\n{\n \"ids\": [\"app_abc1\", \"app_abc2\", \"app_abc3\"],\n \"options\": {\n \"atomic\": false\n }\n}\n```\n\nNote: Batch delete uses an `ids` array (not `records`).\n\n### Response (200 or 207)\n\n**Soft delete** (default):\n\n```json\n{\n \"success\": [\n { \"id\": \"app_abc1\", \"candidateId\": \"cand_101\", \"deletedAt\": \"2025-01-15T12:00:00Z\" },\n { \"id\": \"app_abc2\", \"candidateId\": \"cand_103\", \"deletedAt\": \"2025-01-15T12:00:00Z\" }\n ],\n \"errors\": [],\n \"meta\": {\n \"total\": 2,\n \"succeeded\": 2,\n \"failed\": 0,\n \"atomic\": false\n }\n}\n```\n\n**Hard delete**: Returns objects with only the `id` field (record data is deleted).\n\n## Batch Upsert\n\n```\nPUT /api/v1/{resource}/batch\n```\n\nInserts new records or updates existing ones. Every record must include an `id` field.\n\n**Note:** Batch upsert is only available when `generateId` is set to `false` (user-provided IDs) in your configuration.\n\n### Request\n\n```json\n{\n \"records\": [\n { \"id\": \"app_001\", \"candidateId\": \"cand_101\", \"jobId\": \"job_201\", \"stage\": \"screening\", \"notes\": \"Updated after phone screen\" },\n { \"id\": \"app_002\", \"candidateId\": \"cand_104\", \"jobId\": \"job_203\", \"stage\": \"applied\", \"notes\": \"New application from referral\" }\n ],\n \"options\": {\n \"atomic\": false\n }\n}\n```\n\n### Response (201 or 207)\n\nThe compiler checks which IDs already exist and splits the batch into creates and updates. Create records get ownership and default fields; update records get only `modifiedAt`.\n\n## Atomic Mode\n\nBy default, batch operations use **partial success** mode — each record is processed independently, and failures don't affect other records.\n\nSet `\"atomic\": true` to require all-or-nothing processing:\n\n```json\n{\n \"records\": [...],\n \"options\": {\n \"atomic\": true\n }\n}\n```\n\n### Atomic Failure Response (400)\n\nIf any record fails in atomic mode, the entire batch is rejected:\n\n```json\n{\n \"error\": \"Batch operation failed in atomic mode\",\n \"layer\": \"validation\",\n \"code\": \"BATCH_ATOMIC_FAILED\",\n \"details\": {\n \"failedAt\": 1,\n \"reason\": \"Database insert failed\",\n \"errorDetails\": { \"reason\": \"UNIQUE constraint failed\" }\n },\n \"hint\": \"Transaction rolled back. Fix the error and retry the entire batch.\"\n}\n```\n\n## Batch Size Limit\n\nThe default maximum batch size is **100 records**. Requests exceeding the limit are rejected:\n\n```json\n{\n \"error\": \"Batch size limit exceeded\",\n \"layer\": \"validation\",\n \"code\": \"BATCH_SIZE_EXCEEDED\",\n \"details\": { \"max\": 100, \"actual\": 250 },\n \"hint\": \"Maximum 100 records allowed per batch. Split into multiple requests.\"\n}\n```\n\n## Configuration\n\nBatch operations inherit access control from their corresponding CRUD operation. You can override per-batch settings:\n\n```typescript\nexport default defineTable(applications, {\n crud: {\n create: { access: { roles: ['hiring-manager', 'recruiter'] } },\n update: { access: { roles: ['hiring-manager', 'recruiter'] } },\n delete: { access: { roles: ['hiring-manager'] }, mode: 'soft' },\n\n // Override batch-specific settings\n batchCreate: {\n access: { roles: ['hiring-manager'] }, // More restrictive than single create\n maxBatchSize: 50, // Default: 100\n allowAtomic: true, // Default: true\n },\n batchUpdate: {\n maxBatchSize: 100,\n allowAtomic: true,\n },\n batchDelete: {\n access: { roles: ['hiring-manager'] },\n maxBatchSize: 100,\n allowAtomic: true,\n },\n\n // Disable a batch operation entirely\n batchUpsert: false,\n },\n});\n```\n\n## Security\n\nAll security pillars apply to batch operations:\n\n1. **Authentication** — Required for all batch endpoints (401 if missing)\n2. **Firewall** — Applied per-record for update/delete/upsert (not applicable to create)\n3. **Access** — Role-based check using the batch operation's access config\n4. **Guards** — Per-record validation (createable/updatable/immutable fields)\n5. **Masking** — Applied to all records in the success array before response\n\n## See Also\n\n- [CRUD Endpoints](/compiler/using-the-api/crud) — Single-record operations\n- [Error Responses](/compiler/using-the-api/errors) — Error format reference\n- [Guards](/compiler/definitions/guards) — Field-level write protection"
239
251
  },
240
252
  "compiler/using-the-api/crud": {
241
253
  "title": "CRUD Endpoints",
242
- "content": "Quickback automatically generates RESTful CRUD endpoints for each resource you define. This page covers how to use these endpoints.\n\n## Endpoint Overview\n\nFor a resource named `rooms`, Quickback generates:\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| `GET` | `/rooms` | List all records |\n| `GET` | `/rooms/:id` | Get a single record |\n| `POST` | `/rooms` | Create a new record |\n| `POST` | `/rooms/batch` | Batch create multiple records |\n| `PATCH` | `/rooms/:id` | Update a record |\n| `PATCH` | `/rooms/batch` | Batch update multiple records |\n| `DELETE` | `/rooms/:id` | Delete a record |\n| `DELETE` | `/rooms/batch` | Batch delete multiple records |\n| `PUT` | `/rooms/:id` | Upsert a record (requires config) |\n| `PUT` | `/rooms/batch` | Batch upsert multiple records (requires config) |\n\n## List Records\n\n```\nGET /rooms\n```\n\nReturns a paginated list of records the user has access to.\n\n### Query Parameters\n\n| Parameter | Description | Example |\n|-----------|-------------|---------|\n| `limit` | Number of records to return (default: 50, max: 100) | `?limit=25` |\n| `offset` | Number of records to skip | `?offset=50` |\n| `sort` | Field to sort by | `?sort=createdAt` |\n| `order` | Sort direction: `asc` or `desc` | `?order=desc` |\n\n### Filtering\n\nFilter records using query parameters:\n\n```\nGET /rooms?status=active # Exact match\nGET /rooms?capacity.gt=10 # Greater than\nGET /rooms?capacity.gte=10 # Greater than or equal\nGET /rooms?capacity.lt=50 # Less than\nGET /rooms?capacity.lte=50 # Less than or equal\nGET /rooms?status.ne=deleted # Not equal\nGET /rooms?name.like=Conference # Pattern match (LIKE %value%)\nGET /rooms?status.in=active,pending,review # IN clause\n```\n\n### Response\n\n```json\n{\n \"data\": [\n {\n \"id\": \"room_123\",\n \"name\": \"Conference Room A\",\n \"capacity\": 10,\n \"roomTypeId\": \"rt_456\",\n \"roomType_label\": \"Conference\",\n \"status\": \"active\",\n \"createdAt\": \"2024-01-15T10:30:00Z\"\n }\n ],\n \"pagination\": {\n \"limit\": 50,\n \"offset\": 0,\n \"count\": 1\n }\n}\n```\n\n### FK Label Resolution\n\nForeign key columns are automatically enriched with `_label` fields containing the referenced table's display value. For example, `roomTypeId` gets a corresponding `roomType_label` field. See [Display Column](/compiler/definitions/schema#display-column) for details.\n\n## Get Single Record\n\n```\nGET /rooms/:id\n```\n\nReturns a single record by ID.\n\n### Response\n\n```json\n{\n \"id\": \"room_123\",\n \"name\": \"Conference Room A\",\n \"capacity\": 10,\n \"roomTypeId\": \"rt_456\",\n \"roomType_label\": \"Conference\",\n \"status\": \"active\",\n \"createdAt\": \"2024-01-15T10:30:00Z\"\n}\n```\n\n### Errors\n\n| Status | Description |\n|--------|-------------|\n| `404` | Record not found or not accessible |\n| `403` | User lacks permission to view this record |\n\n## Create Record\n\n```\nPOST /rooms\nContent-Type: application/json\n\n{\n \"name\": \"Conference Room B\",\n \"capacity\": 8,\n \"roomType\": \"meeting\"\n}\n```\n\nCreates a new record. Only fields listed in `guards.createable` are accepted.\n\n### Response\n\n```json\n{\n \"data\": {\n \"id\": \"room_456\",\n \"name\": \"Conference Room B\",\n \"capacity\": 8,\n \"roomType\": \"meeting\",\n \"createdAt\": \"2024-01-15T11:00:00Z\",\n \"createdBy\": \"user_789\"\n }\n}\n```\n\n### Errors\n\n| Status | Description |\n|--------|-------------|\n| `400` | Invalid field or missing required field |\n| `403` | User lacks permission to create records |\n\n## Update Record\n\n```\nPATCH /rooms/:id\nContent-Type: application/json\n\n{\n \"name\": \"Updated Room Name\",\n \"capacity\": 12\n}\n```\n\nUpdates an existing record. Only fields listed in `guards.updatable` are accepted.\n\n### Response\n\n```json\n{\n \"data\": {\n \"id\": \"room_123\",\n \"name\": \"Updated Room Name\",\n \"capacity\": 12,\n \"modifiedAt\": \"2024-01-15T12:00:00Z\",\n \"modifiedBy\": \"user_789\"\n }\n}\n```\n\n### Errors\n\n| Status | Description |\n|--------|-------------|\n| `400` | Invalid field or field not updatable |\n| `403` | User lacks permission to update this record |\n| `404` | Record not found |\n\n## Delete Record\n\n```\nDELETE /rooms/:id\n```\n\nDeletes a record. Behavior depends on the `delete.mode` configuration.\n\n### Soft Delete (default)\n\nSets `deletedAt` and `deletedBy` fields. Record remains in database but is filtered from queries.\n\n### Hard Delete\n\nPermanently removes the record from the database.\n\n### Response\n\n```json\n{\n \"data\": {\n \"id\": \"room_123\",\n \"deleted\": true\n }\n}\n```\n\n### Errors\n\n| Status | Description |\n|--------|-------------|\n| `403` | User lacks permission to delete this record |\n| `404` | Record not found |\n\n## Upsert Record (PUT)\n\n```\nPUT /rooms/:id\nContent-Type: application/json\n\n{\n \"name\": \"External Room\",\n \"capacity\": 20,\n \"externalId\": \"ext-123\"\n}\n```\n\nCreates or updates a record by ID. Requires special configuration:\n\n1. `generateId: false` in database config\n2. `guards: false` in resource definition\n\n### Behavior\n\n- If record exists: Updates all provided fields\n- If record doesn't exist: Creates with the provided ID\n\n### Use Cases\n\n- Syncing data from external systems\n- Webhook handlers with external IDs\n- Idempotent operations (safe to retry)\n\nSee [Guards documentation](/compiler/definitions/guards#putupsert-with-external-ids) for setup details.\n\n## Batch Operations\n\nQuickback provides batch endpoints for efficient bulk operations. Batch operations automatically inherit from their corresponding CRUD operations and maintain full security layer consistency.\n\n### Batch Create Records\n\n```\nPOST /rooms/batch\nContent-Type: application/json\n\n{\n \"records\": [\n { \"name\": \"Room A\", \"capacity\": 10 },\n { \"name\": \"Room B\", \"capacity\": 20 },\n { \"name\": \"Room C\", \"capacity\": 15 }\n ],\n \"options\": {\n \"atomic\": false\n }\n}\n```\n\nCreates multiple records in a single request. Each record follows the same validation rules as single create operations.\n\n#### Request Body\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `records` | Array | Array of record objects to create |\n| `options.atomic` | Boolean | If `true`, all records succeed or all fail (default: `false`) |\n\n#### Response (Partial Success - Default)\n\n```json\n{\n \"success\": [\n { \"id\": \"room_1\", \"name\": \"Room A\", \"capacity\": 10 },\n { \"id\": \"room_2\", \"name\": \"Room B\", \"capacity\": 20 }\n ],\n \"errors\": [\n {\n \"index\": 2,\n \"record\": { \"name\": \"Room C\", \"capacity\": 15 },\n \"error\": {\n \"error\": \"Field cannot be set during creation\",\n \"layer\": \"guards\",\n \"code\": \"GUARD_FIELD_NOT_CREATEABLE\",\n \"details\": { \"fields\": [\"status\"] }\n }\n }\n ],\n \"meta\": {\n \"total\": 3,\n \"succeeded\": 2,\n \"failed\": 1,\n \"atomic\": false\n }\n}\n```\n\n#### HTTP Status Codes\n\n- `201` - All records created successfully\n- `207` - Partial success (some records failed)\n- `400` - Atomic mode enabled and one or more records failed\n\n#### Batch Size Limit\n\nDefault: 100 records per request (configurable via `maxBatchSize`)\n\n### Batch Update Records\n\n```\nPATCH /rooms/batch\nContent-Type: application/json\n\n{\n \"records\": [\n { \"id\": \"room_1\", \"capacity\": 12 },\n { \"id\": \"room_2\", \"name\": \"Updated Room B\" },\n { \"id\": \"room_3\", \"capacity\": 25 }\n ],\n \"options\": {\n \"atomic\": false\n }\n}\n```\n\nUpdates multiple records in a single request. All records must include an `id` field.\n\n#### Request Body\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `records` | Array | Array of record objects with `id` and fields to update |\n| `options.atomic` | Boolean | If `true`, all records succeed or all fail (default: `false`) |\n\n#### Response\n\n```json\n{\n \"success\": [\n { \"id\": \"room_1\", \"capacity\": 12, \"modifiedAt\": \"2024-01-15T14:00:00Z\" },\n { \"id\": \"room_2\", \"name\": \"Updated Room B\", \"modifiedAt\": \"2024-01-15T14:00:00Z\" }\n ],\n \"errors\": [\n {\n \"index\": 2,\n \"record\": { \"id\": \"room_3\", \"capacity\": 25 },\n \"error\": {\n \"error\": \"Not found\",\n \"layer\": \"firewall\",\n \"code\": \"NOT_FOUND\",\n \"details\": { \"id\": \"room_3\" }\n }\n }\n ],\n \"meta\": {\n \"total\": 3,\n \"succeeded\": 2,\n \"failed\": 1,\n \"atomic\": false\n }\n}\n```\n\n#### Features\n\n- **Batch fetching**: Single database query for all IDs (with firewall)\n- **Per-record access**: Access checks run with record context\n- **Field validation**: Guards apply to each record individually\n\n### Batch Delete Records\n\n```\nDELETE /rooms/batch\nContent-Type: application/json\n\n{\n \"ids\": [\"room_1\", \"room_2\", \"room_3\"],\n \"options\": {\n \"atomic\": false\n }\n}\n```\n\nDeletes multiple records in a single request. Supports both soft and hard delete modes.\n\n#### Request Body\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `ids` | Array | Array of record IDs to delete |\n| `options.atomic` | Boolean | If `true`, all records succeed or all fail (default: `false`) |\n\n#### Response (Soft Delete)\n\n```json\n{\n \"success\": [\n { \"id\": \"room_1\", \"deletedAt\": \"2024-01-15T15:00:00Z\", \"deletedBy\": \"user_789\" },\n { \"id\": \"room_2\", \"deletedAt\": \"2024-01-15T15:00:00Z\", \"deletedBy\": \"user_789\" }\n ],\n \"errors\": [\n {\n \"index\": 2,\n \"id\": \"room_3\",\n \"error\": {\n \"error\": \"Not found\",\n \"layer\": \"firewall\",\n \"code\": \"NOT_FOUND\",\n \"details\": { \"id\": \"room_3\" }\n }\n }\n ],\n \"meta\": {\n \"total\": 3,\n \"succeeded\": 2,\n \"failed\": 1,\n \"atomic\": false\n }\n}\n```\n\n#### Delete Modes\n\n- **Soft delete** (default): Sets `deletedAt`, `deletedBy`, `modifiedAt`, `modifiedBy` fields\n- **Hard delete**: Permanently removes records from database\n\n### Batch Upsert Records\n\n```\nPUT /rooms/batch\nContent-Type: application/json\n\n{\n \"records\": [\n { \"id\": \"room_1\", \"name\": \"Updated Room A\", \"capacity\": 10 },\n { \"id\": \"new_room\", \"name\": \"New Room\", \"capacity\": 30 }\n ],\n \"options\": {\n \"atomic\": false\n }\n}\n```\n\nCreates or updates multiple records in a single request. Creates if ID doesn't exist, updates if it does.\n\n**Strict Requirements** (same as single PUT):\n1. `generateId: false` in database config (user provides IDs)\n2. `guards: false` in resource definition (no field restrictions)\n3. All records must include an `id` field\n\n**Note**: System-managed fields (`createdAt`, `createdBy`, `modifiedAt`, `modifiedBy`, `deletedAt`, `deletedBy`) are always protected and will be rejected if included in the request, regardless of guards configuration.\n\n#### Request Body\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `records` | Array | Array of record objects with `id` and all fields |\n| `options.atomic` | Boolean | If `true`, all records succeed or all fail (default: `false`) |\n\n#### Response\n\n```json\n{\n \"success\": [\n { \"id\": \"room_1\", \"name\": \"Updated Room A\", \"capacity\": 10, \"modifiedAt\": \"2024-01-15T16:00:00Z\" },\n { \"id\": \"new_room\", \"name\": \"New Room\", \"capacity\": 30, \"createdAt\": \"2024-01-15T16:00:00Z\" }\n ],\n \"errors\": [],\n \"meta\": {\n \"total\": 2,\n \"succeeded\": 2,\n \"failed\": 0,\n \"atomic\": false\n }\n}\n```\n\n#### How It Works\n\n1. Batch existence check with firewall\n2. Split records into CREATE and UPDATE batches\n3. Validate new records with `validateCreate()`\n4. Validate existing records with `validateUpdate()`\n5. Check CREATE access for new records\n6. Check UPDATE access for existing records (per-record)\n7. Execute bulk insert and individual updates\n8. Return combined results\n\n### Batch Operation Features\n\n#### Partial Success Mode (Default)\n\nBy default, batch operations use **partial success** mode:\n- All records are processed independently\n- Failed records go into `errors` array with detailed error information\n- Successful records go into `success` array\n- HTTP status `207 Multi-Status` if any errors, `201`/`200` if all success\n\n```json\n{\n \"success\": [ /* succeeded records */ ],\n \"errors\": [\n {\n \"index\": 2,\n \"record\": { /* original input */ },\n \"error\": {\n \"error\": \"Human-readable message\",\n \"layer\": \"guards\",\n \"code\": \"GUARD_FIELD_NOT_CREATEABLE\",\n \"details\": { \"fields\": [\"status\"] },\n \"hint\": \"These fields are set automatically or must be omitted\"\n }\n }\n ],\n \"meta\": {\n \"total\": 10,\n \"succeeded\": 8,\n \"failed\": 2,\n \"atomic\": false\n }\n}\n```\n\n#### Atomic Mode (Opt-in)\n\nEnable **atomic mode** for all-or-nothing behavior:\n\n```json\n{\n \"records\": [ /* ... */ ],\n \"options\": {\n \"atomic\": true\n }\n}\n```\n\n**Atomic mode behavior**:\n- First error immediately stops processing\n- All changes are rolled back (database transaction)\n- HTTP status `400 Bad Request`\n- Returns single error with failure details\n\n```json\n{\n \"error\": \"Batch operation failed in atomic mode\",\n \"layer\": \"validation\",\n \"code\": \"BATCH_ATOMIC_FAILED\",\n \"details\": {\n \"failedAt\": 2,\n \"reason\": { /* the actual error */ }\n },\n \"hint\": \"Transaction rolled back. Fix the error and retry the entire batch.\"\n}\n```\n\n#### Human-Readable Errors\n\nAll batch operation errors include:\n- **Layer identification**: Which security layer rejected the request\n- **Error code**: Machine-readable code for programmatic handling\n- **Clear message**: Human-readable explanation\n- **Details**: Contextual information (fields, IDs, reasons)\n- **Helpful hints**: Actionable guidance for resolution\n\n#### Performance Optimizations\n\n- **Batch size limits**: Default 100 records (prevents memory exhaustion)\n- **Single firewall query**: `WHERE id IN (...)` instead of N queries\n- **Bulk operations**: Single INSERT for multiple records (CREATE, UPSERT)\n- **O(1) lookups**: Map-based record lookup instead of Array.find()\n\n#### Configuration\n\nBatch operations are **auto-enabled** when corresponding CRUD operations exist:\n\n```typescript\n// Auto-enabled - no configuration needed\ncrud: {\n create: { access: { roles: ['member'] } },\n update: { access: { roles: ['member'] } }\n // batchCreate and batchUpdate automatically available\n}\n\n// Customize batch operations\ncrud: {\n create: { access: { roles: ['member'] } },\n batchCreate: {\n access: { roles: ['admin'] }, // Different access rules\n maxBatchSize: 50, // Lower limit\n allowAtomic: false // Disable atomic mode\n }\n}\n\n// Disable batch operations\ncrud: {\n create: { access: { roles: ['member'] } },\n batchCreate: false // Explicitly disable\n}\n```\n\n#### Security Layer Application\n\nBatch operations maintain **full security layer consistency**:\n\n1. **Firewall**: Auto-apply ownership fields, batch fetch with isolation\n2. **Access**: Operation-level for CREATE, per-record for UPDATE/DELETE\n3. **Guards**: Per-record field validation (same rules as single operations)\n4. **Masking**: Applied to success array (respects user permissions)\n5. **Audit**: Single timestamp for entire batch for consistency\n\n## Authentication\n\nAll endpoints require authentication. Include your auth token in the request header:\n\n```\nAuthorization: Bearer <your-token>\n```\n\nThe user's context (userId, roles, organizationId) is extracted from the token and used to:\n\n1. Apply firewall filters (data isolation)\n2. Check access permissions\n3. Set audit fields (createdBy, modifiedBy)\n\n## Error Responses\n\nAll errors use a flat structure with contextual fields:\n\n```json\n{\n \"error\": \"Insufficient permissions\",\n \"layer\": \"access\",\n \"code\": \"ACCESS_ROLE_REQUIRED\",\n \"details\": {\n \"required\": [\"admin\"],\n \"current\": [\"member\"]\n },\n \"hint\": \"Contact an administrator to grant necessary permissions\"\n}\n```\n\nSee [Errors](/compiler/using-the-api/errors) for the complete reference of error codes by security layer."
254
+ "content": "Quickback automatically generates RESTful CRUD endpoints for each resource you define. This page covers how to use these endpoints.\n\n## Quick Reference\n\nAll endpoints require authentication. Include your session cookie or Bearer token:\n\n```bash\n# List records\ncurl http://localhost:8787/api/v1/jobs \\\n -H \"Authorization: Bearer <token>\"\n\n# Get a single record\ncurl http://localhost:8787/api/v1/jobs/job_123 \\\n -H \"Authorization: Bearer <token>\"\n\n# Create a record\ncurl -X POST http://localhost:8787/api/v1/jobs \\\n -H \"Authorization: Bearer <token>\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"title\": \"Senior Engineer\", \"department\": \"Engineering\", \"status\": \"open\"}'\n\n# Update a record\ncurl -X PATCH http://localhost:8787/api/v1/jobs/job_123 \\\n -H \"Authorization: Bearer <token>\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"title\": \"Staff Engineer\"}'\n\n# Delete a record\ncurl -X DELETE http://localhost:8787/api/v1/jobs/job_123 \\\n -H \"Authorization: Bearer <token>\"\n\n# List with filters, sorting, and pagination\ncurl \"http://localhost:8787/api/v1/jobs?status=open&sort=createdAt:desc&limit=25&count=true\" \\\n -H \"Authorization: Bearer <token>\"\n```\n\n## Endpoint Overview\n\nFor a resource named `jobs`, Quickback generates:\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| `GET` | `/jobs` | List all records |\n| `GET` | `/jobs/:id` | Get a single record |\n| `POST` | `/jobs` | Create a new record |\n| `POST` | `/jobs/batch` | Batch create multiple records |\n| `PATCH` | `/jobs/:id` | Update a record |\n| `PATCH` | `/jobs/batch` | Batch update multiple records |\n| `DELETE` | `/jobs/:id` | Delete a record |\n| `DELETE` | `/jobs/batch` | Batch delete multiple records |\n| `PUT` | `/jobs/:id` | Upsert a record (requires config) |\n| `PUT` | `/jobs/batch` | Batch upsert multiple records (requires config) |\n\n## List Records\n\n```\nGET /jobs\n```\n\nReturns a paginated list of records the user has access to.\n\n### Query Parameters\n\n| Parameter | Description | Example |\n|-----------|-------------|---------|\n| `limit` | Number of records to return (default: 50, max: 100) | `?limit=25` |\n| `offset` | Number of records to skip | `?offset=50` |\n| `sort` | Field to sort by | `?sort=createdAt` |\n| `order` | Sort direction: `asc` or `desc` | `?order=desc` |\n\n### Filtering\n\nFilter records using query parameters:\n\n```\nGET /jobs?status=open # Exact match\nGET /jobs?salaryMax.gt=100000 # Greater than\nGET /jobs?salaryMin.gte=80000 # Greater than or equal\nGET /jobs?salaryMax.lt=200000 # Less than\nGET /jobs?salaryMax.lte=150000 # Less than or equal\nGET /jobs?status.ne=closed # Not equal\nGET /jobs?title.like=Engineer # Pattern match (LIKE %value%)\nGET /jobs?status.in=open,draft # IN clause\n```\n\n### Response\n\n```json\n{\n \"data\": [\n {\n \"id\": \"job_123\",\n \"title\": \"Senior Engineer\",\n \"department\": \"Engineering\",\n \"status\": \"open\",\n \"salaryMin\": 120000,\n \"salaryMax\": 180000,\n \"createdAt\": \"2024-01-15T10:30:00Z\"\n }\n ],\n \"pagination\": {\n \"limit\": 50,\n \"offset\": 0,\n \"count\": 1\n }\n}\n```\n\n### FK Label Resolution\n\nForeign key columns are automatically enriched with `_label` fields containing the referenced table's display value. For example, `departmentId` gets a corresponding `department_label` field. See [Display Column](/compiler/definitions/schema#display-column) for details.\n\n## Get Single Record\n\n```\nGET /jobs/:id\n```\n\nReturns a single record by ID.\n\n### Response\n\n```json\n{\n \"id\": \"job_123\",\n \"title\": \"Senior Engineer\",\n \"department\": \"Engineering\",\n \"status\": \"open\",\n \"salaryMin\": 120000,\n \"salaryMax\": 180000,\n \"createdAt\": \"2024-01-15T10:30:00Z\"\n}\n```\n\n### Errors\n\n| Status | Description |\n|--------|-------------|\n| `404` | Record not found or not accessible |\n| `403` | User lacks permission to view this record |\n\n## Create Record\n\n```\nPOST /jobs\nContent-Type: application/json\n\n{\n \"title\": \"Senior Engineer\",\n \"department\": \"Engineering\",\n \"status\": \"open\",\n \"salaryMin\": 120000,\n \"salaryMax\": 180000\n}\n```\n\nCreates a new record. Only fields listed in `guards.createable` are accepted.\n\n### Response\n\n```json\n{\n \"data\": {\n \"id\": \"job_456\",\n \"title\": \"Senior Engineer\",\n \"department\": \"Engineering\",\n \"status\": \"open\",\n \"salaryMin\": 120000,\n \"salaryMax\": 180000,\n \"createdAt\": \"2024-01-15T11:00:00Z\",\n \"createdBy\": \"user_789\"\n }\n}\n```\n\n### Errors\n\n| Status | Description |\n|--------|-------------|\n| `400` | Invalid field or missing required field |\n| `403` | User lacks permission to create records |\n\n## Update Record\n\n```\nPATCH /jobs/:id\nContent-Type: application/json\n\n{\n \"title\": \"Staff Engineer\"\n}\n```\n\nUpdates an existing record. Only fields listed in `guards.updatable` are accepted.\n\n### Response\n\n```json\n{\n \"data\": {\n \"id\": \"job_123\",\n \"title\": \"Staff Engineer\",\n \"modifiedAt\": \"2024-01-15T12:00:00Z\",\n \"modifiedBy\": \"user_789\"\n }\n}\n```\n\n### Errors\n\n| Status | Description |\n|--------|-------------|\n| `400` | Invalid field or field not updatable |\n| `403` | User lacks permission to update this record |\n| `404` | Record not found |\n\n## Delete Record\n\n```\nDELETE /jobs/:id\n```\n\nDeletes a record. Behavior depends on the `delete.mode` configuration.\n\n### Soft Delete (default)\n\nSets `deletedAt` and `deletedBy` fields. Record remains in database but is filtered from queries.\n\n### Hard Delete\n\nPermanently removes the record from the database.\n\n### Response\n\n```json\n{\n \"data\": {\n \"id\": \"job_123\",\n \"deleted\": true\n }\n}\n```\n\n### Errors\n\n| Status | Description |\n|--------|-------------|\n| `403` | User lacks permission to delete this record |\n| `404` | Record not found |\n\n## Upsert Record (PUT)\n\n```\nPUT /jobs/:id\nContent-Type: application/json\n\n{\n \"title\": \"Contract Recruiter\",\n \"department\": \"Talent Acquisition\",\n \"status\": \"open\",\n \"externalId\": \"ext-123\"\n}\n```\n\nCreates or updates a record by ID. Requires special configuration:\n\n1. `generateId: false` in database config\n2. `guards: false` in resource definition\n\n### Behavior\n\n- If record exists: Updates all provided fields\n- If record doesn't exist: Creates with the provided ID\n\n### Use Cases\n\n- Syncing data from external systems\n- Webhook handlers with external IDs\n- Idempotent operations (safe to retry)\n\nSee [Guards documentation](/compiler/definitions/guards#putupsert-with-external-ids) for setup details.\n\n## Batch Operations\n\nQuickback provides batch endpoints for efficient bulk operations. Batch operations automatically inherit from their corresponding CRUD operations and maintain full security layer consistency.\n\n### Batch Create Records\n\n```\nPOST /jobs/batch\nContent-Type: application/json\n\n{\n \"records\": [\n { \"title\": \"Frontend Engineer\", \"department\": \"Engineering\", \"status\": \"open\" },\n { \"title\": \"Product Designer\", \"department\": \"Design\", \"status\": \"draft\" },\n { \"title\": \"Data Analyst\", \"department\": \"Analytics\", \"status\": \"open\" }\n ],\n \"options\": {\n \"atomic\": false\n }\n}\n```\n\nCreates multiple records in a single request. Each record follows the same validation rules as single create operations.\n\n#### Request Body\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `records` | Array | Array of record objects to create |\n| `options.atomic` | Boolean | If `true`, all records succeed or all fail (default: `false`) |\n\n#### Response (Partial Success - Default)\n\n```json\n{\n \"success\": [\n { \"id\": \"job_1\", \"title\": \"Frontend Engineer\", \"department\": \"Engineering\", \"status\": \"open\" },\n { \"id\": \"job_2\", \"title\": \"Product Designer\", \"department\": \"Design\", \"status\": \"draft\" }\n ],\n \"errors\": [\n {\n \"index\": 2,\n \"record\": { \"title\": \"Data Analyst\", \"department\": \"Analytics\", \"status\": \"open\" },\n \"error\": {\n \"error\": \"Field cannot be set during creation\",\n \"layer\": \"guards\",\n \"code\": \"GUARD_FIELD_NOT_CREATEABLE\",\n \"details\": { \"fields\": [\"status\"] }\n }\n }\n ],\n \"meta\": {\n \"total\": 3,\n \"succeeded\": 2,\n \"failed\": 1,\n \"atomic\": false\n }\n}\n```\n\n#### HTTP Status Codes\n\n- `201` - All records created successfully\n- `207` - Partial success (some records failed)\n- `400` - Atomic mode enabled and one or more records failed\n\n#### Batch Size Limit\n\nDefault: 100 records per request (configurable via `maxBatchSize`)\n\n### Batch Update Records\n\n```\nPATCH /jobs/batch\nContent-Type: application/json\n\n{\n \"records\": [\n { \"id\": \"job_1\", \"title\": \"Senior Frontend Engineer\" },\n { \"id\": \"job_2\", \"department\": \"Product Design\" },\n { \"id\": \"job_3\", \"status\": \"closed\" }\n ],\n \"options\": {\n \"atomic\": false\n }\n}\n```\n\nUpdates multiple records in a single request. All records must include an `id` field.\n\n#### Request Body\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `records` | Array | Array of record objects with `id` and fields to update |\n| `options.atomic` | Boolean | If `true`, all records succeed or all fail (default: `false`) |\n\n#### Response\n\n```json\n{\n \"success\": [\n { \"id\": \"job_1\", \"title\": \"Senior Frontend Engineer\", \"modifiedAt\": \"2024-01-15T14:00:00Z\" },\n { \"id\": \"job_2\", \"department\": \"Product Design\", \"modifiedAt\": \"2024-01-15T14:00:00Z\" }\n ],\n \"errors\": [\n {\n \"index\": 2,\n \"record\": { \"id\": \"job_3\", \"status\": \"closed\" },\n \"error\": {\n \"error\": \"Not found\",\n \"layer\": \"firewall\",\n \"code\": \"NOT_FOUND\",\n \"details\": { \"id\": \"job_3\" }\n }\n }\n ],\n \"meta\": {\n \"total\": 3,\n \"succeeded\": 2,\n \"failed\": 1,\n \"atomic\": false\n }\n}\n```\n\n#### Features\n\n- **Batch fetching**: Single database query for all IDs (with firewall)\n- **Per-record access**: Access checks run with record context\n- **Field validation**: Guards apply to each record individually\n\n### Batch Delete Records\n\n```\nDELETE /jobs/batch\nContent-Type: application/json\n\n{\n \"ids\": [\"job_1\", \"job_2\", \"job_3\"],\n \"options\": {\n \"atomic\": false\n }\n}\n```\n\nDeletes multiple records in a single request. Supports both soft and hard delete modes.\n\n#### Request Body\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `ids` | Array | Array of record IDs to delete |\n| `options.atomic` | Boolean | If `true`, all records succeed or all fail (default: `false`) |\n\n#### Response (Soft Delete)\n\n```json\n{\n \"success\": [\n { \"id\": \"job_1\", \"deletedAt\": \"2024-01-15T15:00:00Z\", \"deletedBy\": \"user_789\" },\n { \"id\": \"job_2\", \"deletedAt\": \"2024-01-15T15:00:00Z\", \"deletedBy\": \"user_789\" }\n ],\n \"errors\": [\n {\n \"index\": 2,\n \"id\": \"job_3\",\n \"error\": {\n \"error\": \"Not found\",\n \"layer\": \"firewall\",\n \"code\": \"NOT_FOUND\",\n \"details\": { \"id\": \"job_3\" }\n }\n }\n ],\n \"meta\": {\n \"total\": 3,\n \"succeeded\": 2,\n \"failed\": 1,\n \"atomic\": false\n }\n}\n```\n\n#### Delete Modes\n\n- **Soft delete** (default): Sets `deletedAt`, `deletedBy`, `modifiedAt`, `modifiedBy` fields\n- **Hard delete**: Permanently removes records from database\n\n### Batch Upsert Records\n\n```\nPUT /jobs/batch\nContent-Type: application/json\n\n{\n \"records\": [\n { \"id\": \"job_1\", \"title\": \"Updated Frontend Engineer\", \"department\": \"Engineering\" },\n { \"id\": \"new_job\", \"title\": \"New Backend Engineer\", \"department\": \"Engineering\", \"status\": \"draft\" }\n ],\n \"options\": {\n \"atomic\": false\n }\n}\n```\n\nCreates or updates multiple records in a single request. Creates if ID doesn't exist, updates if it does.\n\n**Strict Requirements** (same as single PUT):\n1. `generateId: false` in database config (user provides IDs)\n2. `guards: false` in resource definition (no field restrictions)\n3. All records must include an `id` field\n\n**Note**: System-managed fields (`createdAt`, `createdBy`, `modifiedAt`, `modifiedBy`, `deletedAt`, `deletedBy`) are always protected and will be rejected if included in the request, regardless of guards configuration.\n\n#### Request Body\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `records` | Array | Array of record objects with `id` and all fields |\n| `options.atomic` | Boolean | If `true`, all records succeed or all fail (default: `false`) |\n\n#### Response\n\n```json\n{\n \"success\": [\n { \"id\": \"job_1\", \"title\": \"Updated Frontend Engineer\", \"department\": \"Engineering\", \"modifiedAt\": \"2024-01-15T16:00:00Z\" },\n { \"id\": \"new_job\", \"title\": \"New Backend Engineer\", \"department\": \"Engineering\", \"status\": \"draft\", \"createdAt\": \"2024-01-15T16:00:00Z\" }\n ],\n \"errors\": [],\n \"meta\": {\n \"total\": 2,\n \"succeeded\": 2,\n \"failed\": 0,\n \"atomic\": false\n }\n}\n```\n\n#### How It Works\n\n1. Batch existence check with firewall\n2. Split records into CREATE and UPDATE batches\n3. Validate new records with `validateCreate()`\n4. Validate existing records with `validateUpdate()`\n5. Check CREATE access for new records\n6. Check UPDATE access for existing records (per-record)\n7. Execute bulk insert and individual updates\n8. Return combined results\n\n### Batch Operation Features\n\n#### Partial Success Mode (Default)\n\nBy default, batch operations use **partial success** mode:\n- All records are processed independently\n- Failed records go into `errors` array with detailed error information\n- Successful records go into `success` array\n- HTTP status `207 Multi-Status` if any errors, `201`/`200` if all success\n\n```json\n{\n \"success\": [ /* succeeded records */ ],\n \"errors\": [\n {\n \"index\": 2,\n \"record\": { /* original input */ },\n \"error\": {\n \"error\": \"Human-readable message\",\n \"layer\": \"guards\",\n \"code\": \"GUARD_FIELD_NOT_CREATEABLE\",\n \"details\": { \"fields\": [\"status\"] },\n \"hint\": \"These fields are set automatically or must be omitted\"\n }\n }\n ],\n \"meta\": {\n \"total\": 10,\n \"succeeded\": 8,\n \"failed\": 2,\n \"atomic\": false\n }\n}\n```\n\n#### Atomic Mode (Opt-in)\n\nEnable **atomic mode** for all-or-nothing behavior:\n\n```json\n{\n \"records\": [ /* ... */ ],\n \"options\": {\n \"atomic\": true\n }\n}\n```\n\n**Atomic mode behavior**:\n- First error immediately stops processing\n- All changes are rolled back (database transaction)\n- HTTP status `400 Bad Request`\n- Returns single error with failure details\n\n```json\n{\n \"error\": \"Batch operation failed in atomic mode\",\n \"layer\": \"validation\",\n \"code\": \"BATCH_ATOMIC_FAILED\",\n \"details\": {\n \"failedAt\": 2,\n \"reason\": { /* the actual error */ }\n },\n \"hint\": \"Transaction rolled back. Fix the error and retry the entire batch.\"\n}\n```\n\n#### Human-Readable Errors\n\nAll batch operation errors include:\n- **Layer identification**: Which security layer rejected the request\n- **Error code**: Machine-readable code for programmatic handling\n- **Clear message**: Human-readable explanation\n- **Details**: Contextual information (fields, IDs, reasons)\n- **Helpful hints**: Actionable guidance for resolution\n\n#### Performance Optimizations\n\n- **Batch size limits**: Default 100 records (prevents memory exhaustion)\n- **Single firewall query**: `WHERE id IN (...)` instead of N queries\n- **Bulk operations**: Single INSERT for multiple records (CREATE, UPSERT)\n- **O(1) lookups**: Map-based record lookup instead of Array.find()\n\n#### Configuration\n\nBatch operations are **auto-enabled** when corresponding CRUD operations exist:\n\n```typescript\n// Auto-enabled - no configuration needed\ncrud: {\n create: { access: { roles: ['recruiter'] } },\n update: { access: { roles: ['recruiter'] } }\n // batchCreate and batchUpdate automatically available\n}\n\n// Customize batch operations\ncrud: {\n create: { access: { roles: ['recruiter'] } },\n batchCreate: {\n access: { roles: ['hiring-manager'] }, // Different access rules\n maxBatchSize: 50, // Lower limit\n allowAtomic: false // Disable atomic mode\n }\n}\n\n// Disable batch operations\ncrud: {\n create: { access: { roles: ['recruiter'] } },\n batchCreate: false // Explicitly disable\n}\n```\n\n#### Security Layer Application\n\nBatch operations maintain **full security layer consistency**:\n\n1. **Firewall**: Auto-apply ownership fields, batch fetch with isolation\n2. **Access**: Operation-level for CREATE, per-record for UPDATE/DELETE\n3. **Guards**: Per-record field validation (same rules as single operations)\n4. **Masking**: Applied to success array (respects user permissions)\n5. **Audit**: Single timestamp for entire batch for consistency\n\n## Authentication\n\nAll endpoints require authentication. Include your auth token in the request header:\n\n```\nAuthorization: Bearer <your-token>\n```\n\nThe user's context (userId, roles, organizationId) is extracted from the token and used to:\n\n1. Apply firewall filters (data isolation)\n2. Check access permissions\n3. Set audit fields (createdBy, modifiedBy)\n\n## Error Responses\n\nAll errors use a flat structure with contextual fields:\n\n```json\n{\n \"error\": \"Insufficient permissions\",\n \"layer\": \"access\",\n \"code\": \"ACCESS_ROLE_REQUIRED\",\n \"details\": {\n \"required\": [\"hiring-manager\"],\n \"current\": [\"interviewer\"]\n },\n \"hint\": \"Contact an administrator to grant necessary permissions\"\n}\n```\n\nSee [Errors](/compiler/using-the-api/errors) for the complete reference of error codes by security layer."
243
255
  },
244
256
  "compiler/using-the-api/errors": {
245
257
  "title": "Errors",
246
- "content": "The generated API returns structured error responses with consistent fields across all security layers.\n\n## Status Codes\n\n| Code | Meaning | When |\n|------|---------|------|\n| `200` | OK | Successful GET, PATCH, DELETE |\n| `201` | Created | Successful POST |\n| `207` | Multi-Status | Batch operation with mixed results |\n| `400` | Bad Request | Guard violation, validation error, invalid input |\n| `401` | Unauthorized | Missing or expired authentication |\n| `403` | Forbidden | Access denied (wrong role, condition failed) or firewall blocked it |\n| `404` | Not Found | Record doesn't exist (or firewall blocked with `errorMode: 'hide'`) |\n| `429` | Too Many Requests | Rate limit exceeded |\n\n## Error Response Format\n\nAll errors use a flat structure with contextual fields:\n\n```json\n{\n \"error\": \"Insufficient permissions\",\n \"layer\": \"access\",\n \"code\": \"ACCESS_ROLE_REQUIRED\",\n \"details\": {\n \"required\": [\"admin\"],\n \"current\": [\"member\"]\n },\n \"hint\": \"Contact an administrator to grant necessary permissions\"\n}\n```\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `error` | string | Human-readable error message |\n| `layer` | string | Security layer that rejected the request |\n| `code` | string | Machine-readable error code |\n| `details` | object | Layer-specific context (optional) |\n| `hint` | string | Actionable guidance for resolution (optional) |\n\n## Errors by Security Layer\n\n### Authentication (401)\n\nMissing or invalid authentication tokens.\n\n```json\n{\n \"error\": \"Authentication required\",\n \"layer\": \"authentication\",\n \"code\": \"AUTH_MISSING\",\n \"hint\": \"Include Authorization header with Bearer token\"\n}\n```\n\n**Error codes:**\n\n| Code | Description |\n|------|-------------|\n| `AUTH_MISSING` | No Authorization header provided |\n| `AUTH_INVALID_TOKEN` | Token is malformed or invalid |\n| `AUTH_EXPIRED` | Token has expired |\n| `AUTH_RATE_LIMITED` | Too many auth attempts |\n\n### Firewall (403)\n\nRecords outside the user's firewall scope return **403 Forbidden** by default with a structured error:\n\n```json\n{\n \"error\": \"Record not found or not accessible\",\n \"layer\": \"firewall\",\n \"code\": \"FIREWALL_NOT_FOUND\",\n \"hint\": \"Check the record ID and your organization membership\"\n}\n```\n\nFirewall filtering is transparent — the query is scoped by `WHERE organizationId = ?` so inaccessible records simply don't appear in results.\n\n**Error codes:**\n\n| Code | Description |\n|------|-------------|\n| `FIREWALL_NOT_FOUND` | Record not found behind firewall (wrong org, soft-deleted, or doesn't exist) |\n| `FIREWALL_ORG_ISOLATION` | Record belongs to a different organization |\n| `FIREWALL_USER_ISOLATION` | Record belongs to another user |\n| `FIREWALL_SOFT_DELETED` | Record has been soft deleted |\n\nFor security-hardened deployments, set `errorMode: 'hide'` in your firewall config to return opaque **404 Not Found** responses instead. This prevents attackers from distinguishing between \"record exists but you can't access it\" and \"record doesn't exist\".\n\n### Access (403)\n\nAccess violations return **403 Forbidden** when the user's role doesn't match the required roles for the operation.\n\n```json\n{\n \"error\": \"Insufficient permissions\",\n \"layer\": \"access\",\n \"code\": \"ACCESS_ROLE_REQUIRED\",\n \"details\": {\n \"required\": [\"admin\"],\n \"current\": [\"member\"]\n },\n \"hint\": \"Contact an administrator to grant necessary permissions\"\n}\n```\n\n**Error codes:**\n\n| Code | Description |\n|------|-------------|\n| `ACCESS_ROLE_REQUIRED` | User doesn't have the required role |\n| `ACCESS_CONDITION_FAILED` | Record-level access condition not met |\n| `ACCESS_OWNERSHIP_REQUIRED` | User must own the record |\n| `ACCESS_NO_ORG` | No active organization set |\n\n### Guards (400)\n\nGuard violations return **400 Bad Request** when the request body contains fields that aren't allowed.\n\n```json\n{\n \"error\": \"Field cannot be set during creation\",\n \"layer\": \"guards\",\n \"code\": \"GUARD_FIELD_NOT_CREATEABLE\",\n \"details\": {\n \"fields\": [\"status\"]\n },\n \"hint\": \"These fields are set automatically or must be omitted\"\n}\n```\n\n**Error codes:**\n\n| Code | Description |\n|------|-------------|\n| `GUARD_FIELD_NOT_CREATEABLE` | Field not in `createable` list |\n| `GUARD_FIELD_NOT_UPDATABLE` | Field not in `updatable` list |\n| `GUARD_FIELD_PROTECTED` | Field is action-only (protected) |\n| `GUARD_FIELD_IMMUTABLE` | Field cannot be modified after creation |\n| `GUARD_SYSTEM_MANAGED` | System field (createdAt, modifiedAt, etc.) |\n\n### Masking\n\nMasking doesn't produce errors — it silently transforms field values in the response.\n\n## Batch Errors\n\nBatch operations can return:\n- **201** — All records succeeded\n- **207** — Partial success (some records failed)\n- **400** — Atomic mode and at least one record failed (all rolled back)\n\n### Partial Success (207)\n\n```json\n{\n \"success\": [{ \"id\": \"room_1\", \"name\": \"Room A\" }],\n \"errors\": [\n {\n \"index\": 1,\n \"record\": { \"name\": \"Room B\", \"status\": \"active\" },\n \"error\": {\n \"error\": \"Field cannot be set during creation\",\n \"layer\": \"guards\",\n \"code\": \"GUARD_FIELD_NOT_CREATEABLE\",\n \"details\": { \"fields\": [\"status\"] },\n \"hint\": \"These fields are set automatically or must be omitted\"\n }\n }\n ],\n \"meta\": { \"total\": 2, \"succeeded\": 1, \"failed\": 1, \"atomic\": false }\n}\n```\n\n### Atomic Failure (400)\n\n```json\n{\n \"error\": \"Batch operation failed in atomic mode\",\n \"layer\": \"validation\",\n \"code\": \"BATCH_ATOMIC_FAILED\",\n \"details\": {\n \"failedAt\": 2,\n \"reason\": { \"error\": \"Not found\", \"code\": \"NOT_FOUND\" }\n },\n \"hint\": \"Transaction rolled back. Fix the error and retry the entire batch.\"\n}\n```\n\n**Batch error codes:**\n\n| Code | Description |\n|------|-------------|\n| `BATCH_SIZE_EXCEEDED` | Too many records in a single request |\n| `BATCH_ATOMIC_FAILED` | Atomic batch failed, all changes rolled back |\n| `BATCH_MISSING_IDS` | Batch update/delete missing required IDs |"
258
+ "content": "The generated API returns structured error responses with consistent fields across all security layers.\n\n## Status Codes\n\n| Code | Meaning | When |\n|------|---------|------|\n| `200` | OK | Successful GET, PATCH, DELETE |\n| `201` | Created | Successful POST |\n| `207` | Multi-Status | Batch operation with mixed results |\n| `400` | Bad Request | Guard violation, validation error, invalid input |\n| `401` | Unauthorized | Missing or expired authentication |\n| `403` | Forbidden | Access denied (wrong role, condition failed) or firewall blocked it |\n| `404` | Not Found | Record doesn't exist (or firewall blocked with `errorMode: 'hide'`) |\n| `429` | Too Many Requests | Rate limit exceeded |\n\n## Error Response Format\n\nAll errors use a flat structure with contextual fields:\n\n```json\n{\n \"error\": \"Insufficient permissions\",\n \"layer\": \"access\",\n \"code\": \"ACCESS_ROLE_REQUIRED\",\n \"details\": {\n \"required\": [\"hiring-manager\"],\n \"current\": [\"interviewer\"]\n },\n \"hint\": \"Contact an administrator to grant necessary permissions\"\n}\n```\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `error` | string | Human-readable error message |\n| `layer` | string | Security layer that rejected the request |\n| `code` | string | Machine-readable error code |\n| `details` | object | Layer-specific context (optional) |\n| `hint` | string | Actionable guidance for resolution (optional) |\n\n## Errors by Security Layer\n\n### Authentication (401)\n\nMissing or invalid authentication tokens.\n\n```json\n{\n \"error\": \"Authentication required\",\n \"layer\": \"authentication\",\n \"code\": \"AUTH_MISSING\",\n \"hint\": \"Include Authorization header with Bearer token\"\n}\n```\n\n**Error codes:**\n\n| Code | Description |\n|------|-------------|\n| `AUTH_MISSING` | No Authorization header provided |\n| `AUTH_INVALID_TOKEN` | Token is malformed or invalid |\n| `AUTH_EXPIRED` | Token has expired |\n| `AUTH_RATE_LIMITED` | Too many auth attempts |\n\n### Firewall (403)\n\nRecords outside the user's firewall scope return **403 Forbidden** by default with a structured error:\n\n```json\n{\n \"error\": \"Record not found or not accessible\",\n \"layer\": \"firewall\",\n \"code\": \"FIREWALL_NOT_FOUND\",\n \"hint\": \"Check the record ID and your organization membership\"\n}\n```\n\nFirewall filtering is transparent — the query is scoped by `WHERE organizationId = ?` so inaccessible records simply don't appear in results.\n\n**Error codes:**\n\n| Code | Description |\n|------|-------------|\n| `FIREWALL_NOT_FOUND` | Record not found behind firewall (wrong org, soft-deleted, or doesn't exist) |\n| `FIREWALL_ORG_ISOLATION` | Record belongs to a different organization |\n| `FIREWALL_USER_ISOLATION` | Record belongs to another user |\n| `FIREWALL_SOFT_DELETED` | Record has been soft deleted |\n\nFor security-hardened deployments, set `errorMode: 'hide'` in your firewall config to return opaque **404 Not Found** responses instead. This prevents attackers from distinguishing between \"record exists but you can't access it\" and \"record doesn't exist\".\n\n### Access (403)\n\nAccess violations return **403 Forbidden** when the user's role doesn't match the required roles for the operation.\n\n```json\n{\n \"error\": \"Insufficient permissions\",\n \"layer\": \"access\",\n \"code\": \"ACCESS_ROLE_REQUIRED\",\n \"details\": {\n \"required\": [\"hiring-manager\"],\n \"current\": [\"interviewer\"]\n },\n \"hint\": \"Contact an administrator to grant necessary permissions\"\n}\n```\n\n**Error codes:**\n\n| Code | Description |\n|------|-------------|\n| `ACCESS_ROLE_REQUIRED` | User doesn't have the required role |\n| `ACCESS_CONDITION_FAILED` | Record-level access condition not met |\n| `ACCESS_OWNERSHIP_REQUIRED` | User must own the record |\n| `ACCESS_NO_ORG` | No active organization set |\n\n### Guards (400)\n\nGuard violations return **400 Bad Request** when the request body contains fields that aren't allowed.\n\n```json\n{\n \"error\": \"Field cannot be set during creation\",\n \"layer\": \"guards\",\n \"code\": \"GUARD_FIELD_NOT_CREATEABLE\",\n \"details\": {\n \"fields\": [\"stage\"]\n },\n \"hint\": \"These fields are set automatically or must be omitted\"\n}\n```\n\n**Error codes:**\n\n| Code | Description |\n|------|-------------|\n| `GUARD_FIELD_NOT_CREATEABLE` | Field not in `createable` list |\n| `GUARD_FIELD_NOT_UPDATABLE` | Field not in `updatable` list |\n| `GUARD_FIELD_PROTECTED` | Field is action-only (protected) |\n| `GUARD_FIELD_IMMUTABLE` | Field cannot be modified after creation |\n| `GUARD_SYSTEM_MANAGED` | System field (createdAt, modifiedAt, etc.) |\n\n### Masking\n\nMasking doesn't produce errors — it silently transforms field values in the response.\n\n## Batch Errors\n\nBatch operations can return:\n- **201** — All records succeeded\n- **207** — Partial success (some records failed)\n- **400** — Atomic mode and at least one record failed (all rolled back)\n\n### Partial Success (207)\n\n```json\n{\n \"success\": [{ \"id\": \"app_1\", \"candidateId\": \"cand_101\", \"stage\": \"applied\" }],\n \"errors\": [\n {\n \"index\": 1,\n \"record\": { \"candidateId\": \"cand_102\", \"jobId\": \"job_201\", \"stage\": \"interview\" },\n \"error\": {\n \"error\": \"Field cannot be set during creation\",\n \"layer\": \"guards\",\n \"code\": \"GUARD_FIELD_NOT_CREATEABLE\",\n \"details\": { \"fields\": [\"stage\"] },\n \"hint\": \"These fields are set automatically or must be omitted\"\n }\n }\n ],\n \"meta\": { \"total\": 2, \"succeeded\": 1, \"failed\": 1, \"atomic\": false }\n}\n```\n\n### Atomic Failure (400)\n\n```json\n{\n \"error\": \"Batch operation failed in atomic mode\",\n \"layer\": \"validation\",\n \"code\": \"BATCH_ATOMIC_FAILED\",\n \"details\": {\n \"failedAt\": 2,\n \"reason\": { \"error\": \"Not found\", \"code\": \"NOT_FOUND\" }\n },\n \"hint\": \"Transaction rolled back. Fix the error and retry the entire batch.\"\n}\n```\n\n**Batch error codes:**\n\n| Code | Description |\n|------|-------------|\n| `BATCH_SIZE_EXCEEDED` | Too many records in a single request |\n| `BATCH_ATOMIC_FAILED` | Atomic batch failed, all changes rolled back |\n| `BATCH_MISSING_IDS` | Batch update/delete missing required IDs |"
247
259
  },
248
260
  "compiler/using-the-api": {
249
261
  "title": "Using the API",
250
- "content": "When you run `quickback compile`, Quickback generates a complete REST API from your definitions. This section covers how to use the generated endpoints.\n\n## Base URL\n\nAll generated endpoints are served under `/api/v1/`:\n\n```\nhttps://your-api.com/api/v1/{resource}\n```\n\nThe resource name is derived from the filename: `rooms.ts` → `/api/v1/rooms`.\n\n## Endpoint Overview\n\nFor each resource with `export default defineTable(...)`:\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| `GET` | `/api/v1/{resource}` | List records (paginated) |\n| `GET` | `/api/v1/{resource}/:id` | Get single record |\n| `POST` | `/api/v1/{resource}` | Create record |\n| `PATCH` | `/api/v1/{resource}/:id` | Update record |\n| `DELETE` | `/api/v1/{resource}/:id` | Delete record |\n| `POST` | `/api/v1/{resource}/batch` | Batch create |\n| `PATCH` | `/api/v1/{resource}/batch` | Batch update |\n| `DELETE` | `/api/v1/{resource}/batch` | Batch delete |\n\n## Guides\n\n- [CRUD Endpoints](/compiler/using-the-api/crud) — Detailed CRUD endpoint reference\n- [Query Parameters](/compiler/using-the-api/query-params) — Filtering, pagination, and sorting\n- [Batch Operations](/compiler/using-the-api/batch-operations) — Bulk create, update, delete with atomic mode\n- [Views API](/compiler/using-the-api/views-api) — Column-level projections\n- [Actions API](/compiler/using-the-api/actions-api) — Custom business logic endpoints\n- [Errors](/compiler/using-the-api/errors) — Error responses and HTTP status codes"
262
+ "content": "When you run `quickback compile`, Quickback generates a complete REST API from your definitions. This section covers how to use the generated endpoints.\n\n## Base URL\n\nAll generated endpoints are served under `/api/v1/`:\n\n```\nhttps://your-api.com/api/v1/{resource}\n```\n\nThe resource name is derived from the filename: `jobs.ts` → `/api/v1/jobs`.\n\n## Endpoint Overview\n\nFor each resource with `export default defineTable(...)`:\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| `GET` | `/api/v1/{resource}` | List records (paginated) |\n| `GET` | `/api/v1/{resource}/:id` | Get single record |\n| `POST` | `/api/v1/{resource}` | Create record |\n| `PATCH` | `/api/v1/{resource}/:id` | Update record |\n| `DELETE` | `/api/v1/{resource}/:id` | Delete record |\n| `POST` | `/api/v1/{resource}/batch` | Batch create |\n| `PATCH` | `/api/v1/{resource}/batch` | Batch update |\n| `DELETE` | `/api/v1/{resource}/batch` | Batch delete |\n\n## Guides\n\n- [CRUD Endpoints](/compiler/using-the-api/crud) — Detailed CRUD endpoint reference\n- [Query Parameters](/compiler/using-the-api/query-params) — Filtering, pagination, and sorting\n- [Batch Operations](/compiler/using-the-api/batch-operations) — Bulk create, update, delete with atomic mode\n- [Views API](/compiler/using-the-api/views-api) — Column-level projections\n- [Actions API](/compiler/using-the-api/actions-api) — Custom business logic endpoints\n- [Errors](/compiler/using-the-api/errors) — Error responses and HTTP status codes"
251
263
  },
252
264
  "compiler/using-the-api/openapi": {
253
265
  "title": "OpenAPI Spec",
254
- "content": "The compiler generates an [OpenAPI 3.1](https://spec.openapis.org/oas/v3.1.0) specification from your feature definitions. By default it's both written to `openapi.json` at your project root and served as a runtime route.\n\n## Endpoint\n\n```\nGET /openapi.json\n```\n\nReturns the full OpenAPI spec as JSON.\n\n```bash\ncurl https://your-api.example.com/openapi.json\n```\n\n## What's included\n\nThe generated spec documents every route the compiler produces:\n\n- CRUD endpoints (list, get, create, update, delete)\n- Batch operations\n- Views\n- Actions (including standalone actions)\n- Auth routes (`/api/auth/**`)\n- Storage, embeddings, and webhook routes (when configured)\n\nEach endpoint includes request/response schemas derived from your Drizzle column types, access control metadata, and error responses.\n\n## Configuration\n\nBoth generation and publishing default to `true`. You can control them independently in `quickback.config.ts`:\n\n```typescript\nexport default defineConfig({\n name: \"my-app\",\n // ...\n openapi: {\n generate: true, // write openapi.json to project root\n publish: true, // serve GET /openapi.json at runtime\n },\n});\n```\n\n| Option | Default | Description |\n|--------|---------|-------------|\n| `generate` | `true` | Write `openapi.json` to the project root during compilation |\n| `publish` | `true` | Serve the spec at `GET /openapi.json` (requires `generate: true`) |\n\nOmitting the `openapi` key entirely is equivalent to both being `true`.\n\n### Generate only (no runtime route)\n\n```typescript\nopenapi: {\n generate: true,\n publish: false,\n}\n```\n\nThe file is still written so you can use it in CI or commit it to your repo, but no route is added to the app.\n\n### Disable entirely\n\n```typescript\nopenapi: {\n generate: false,\n}\n```\n\nNo file is written and no route is served.\n\n## Usage with tools\n\n### Import into Postman\n\n1. Open Postman and click **Import**\n2. Paste the URL `https://your-api.example.com/openapi.json` or upload the file\n3. Postman creates a collection with every endpoint pre-configured\n\n### Generate a typed client\n\nUse [openapi-typescript](https://github.com/openapi-ts/openapi-typescript) to generate TypeScript types from the spec:\n\n```bash\nnpx openapi-typescript https://your-api.example.com/openapi.json -o src/api.d.ts\n```\n\n### Swagger UI\n\nPoint any OpenAPI-compatible viewer at the endpoint:\n\n```\nhttps://petstore.swagger.io/?url=https://your-api.example.com/openapi.json\n```\n\n## Example output (excerpt)\n\n```json\n{\n \"openapi\": \"3.1.0\",\n \"info\": {\n \"title\": \"my-app API\",\n \"version\": \"1.0.0\"\n },\n \"paths\": {\n \"/api/v1/todos\": {\n \"get\": {\n \"summary\": \"List todos\",\n \"operationId\": \"listTodos\",\n \"parameters\": [\n { \"name\": \"limit\", \"in\": \"query\", \"schema\": { \"type\": \"integer\" } },\n { \"name\": \"offset\", \"in\": \"query\", \"schema\": { \"type\": \"integer\" } }\n ],\n \"responses\": {\n \"200\": {\n \"description\": \"Success\",\n \"content\": {\n \"application/json\": {\n \"schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"data\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/Todo\" } }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n}\n```"
266
+ "content": "The compiler generates an [OpenAPI 3.1](https://spec.openapis.org/oas/v3.1.0) specification from your feature definitions. By default it's both written to `openapi.json` at your project root and served as a runtime route.\n\n## Endpoint\n\n```\nGET /openapi.json\n```\n\nReturns the full OpenAPI spec as JSON.\n\n```bash\ncurl https://your-api.example.com/openapi.json\n```\n\n## What's included\n\nThe generated spec documents every route the compiler produces:\n\n- CRUD endpoints (list, get, create, update, delete)\n- Batch operations\n- Views\n- Actions (including standalone actions)\n- Auth routes (`/api/auth/**`)\n- Storage, embeddings, and webhook routes (when configured)\n\nEach endpoint includes request/response schemas derived from your Drizzle column types, access control metadata, and error responses.\n\n## Configuration\n\nBoth generation and publishing default to `true`. You can control them independently in `quickback.config.ts`:\n\n```typescript\nexport default defineConfig({\n name: \"my-app\",\n // ...\n openapi: {\n generate: true, // write openapi.json to project root\n publish: true, // serve GET /openapi.json at runtime\n },\n});\n```\n\n| Option | Default | Description |\n|--------|---------|-------------|\n| `generate` | `true` | Write `openapi.json` to the project root during compilation |\n| `publish` | `true` | Serve the spec at `GET /openapi.json` (requires `generate: true`) |\n\nOmitting the `openapi` key entirely is equivalent to both being `true`.\n\n### Generate only (no runtime route)\n\n```typescript\nopenapi: {\n generate: true,\n publish: false,\n}\n```\n\nThe file is still written so you can use it in CI or commit it to your repo, but no route is added to the app.\n\n### Disable entirely\n\n```typescript\nopenapi: {\n generate: false,\n}\n```\n\nNo file is written and no route is served.\n\n## Usage with tools\n\n### Import into Postman\n\n1. Open Postman and click **Import**\n2. Paste the URL `https://your-api.example.com/openapi.json` or upload the file\n3. Postman creates a collection with every endpoint pre-configured\n\n### Generate a typed client\n\nUse [openapi-typescript](https://github.com/openapi-ts/openapi-typescript) to generate TypeScript types from the spec:\n\n```bash\nnpx openapi-typescript https://your-api.example.com/openapi.json -o src/api.d.ts\n```\n\n### Swagger UI\n\nPoint any OpenAPI-compatible viewer at the endpoint:\n\n```\nhttps://petstore.swagger.io/?url=https://your-api.example.com/openapi.json\n```\n\n## Example output (excerpt)\n\n```json\n{\n \"openapi\": \"3.1.0\",\n \"info\": {\n \"title\": \"my-app API\",\n \"version\": \"1.0.0\"\n },\n \"paths\": {\n \"/api/v1/jobs\": {\n \"get\": {\n \"summary\": \"List jobs\",\n \"operationId\": \"listJobs\",\n \"parameters\": [\n { \"name\": \"limit\", \"in\": \"query\", \"schema\": { \"type\": \"integer\" } },\n { \"name\": \"offset\", \"in\": \"query\", \"schema\": { \"type\": \"integer\" } }\n ],\n \"responses\": {\n \"200\": {\n \"description\": \"Success\",\n \"content\": {\n \"application/json\": {\n \"schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"data\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/Job\" } }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n}\n```"
255
267
  },
256
268
  "compiler/using-the-api/query-params": {
257
269
  "title": "Query Parameters",
258
- "content": "The generated API supports filtering, pagination, sorting, field selection, search, and total count via query parameters on `GET` list endpoints.\n\n## Filter Operators\n\n| Operator | Query Param | SQL Equivalent |\n|----------|-------------|----------------|\n| Equals | `?field=value` | `WHERE field = value` |\n| Not equals | `?field.ne=value` | `WHERE field != value` |\n| Greater than | `?field.gt=value` | `WHERE field > value` |\n| Greater or equal | `?field.gte=value` | `WHERE field >= value` |\n| Less than | `?field.lt=value` | `WHERE field < value` |\n| Less or equal | `?field.lte=value` | `WHERE field <= value` |\n| Pattern match | `?field.like=value` | `WHERE field LIKE '%value%'` |\n| In list | `?field.in=a,b,c` | `WHERE field IN ('a','b','c')` |\n\n### Examples\n\n```bash\n# Filter by status\nGET /api/v1/rooms?status=active\n\n# Range query\nGET /api/v1/rooms?capacity.gte=10&capacity.lte=50\n\n# Pattern matching\nGET /api/v1/rooms?name.like=conference\n\n# Multiple values\nGET /api/v1/rooms?roomType.in=meeting,conference,workshop\n```\n\n## Pagination\n\n| Parameter | Default | Description |\n|-----------|---------|-------------|\n| `limit` | `50` | Number of records to return (min: 1, max: 100) |\n| `offset` | `0` | Number of records to skip |\n\n```bash\nGET /api/v1/rooms?limit=25&offset=50\n```\n\nThe default `limit` can be configured per-resource in your definition:\n\n```typescript\ncrud: {\n list: {\n access: { roles: [\"member\"] },\n pageSize: 25, // Default limit\n maxPageSize: 100, // Maximum allowed limit\n },\n},\n```\n\n### Response Shape\n\n```json\n{\n \"data\": [ /* records */ ],\n \"pagination\": {\n \"limit\": 25,\n \"offset\": 50,\n \"count\": 12\n }\n}\n```\n\n- `count` — number of records returned on this page\n- `total` — total matching records across all pages (only when `?total=true`, see below)\n\n## Sorting\n\nSort by one or more fields. Use the `-` prefix for descending order.\n\n### Multi-Sort (Recommended)\n\n```bash\n# Sort by status ascending, then createdAt descending\nGET /api/v1/rooms?sort=status,-createdAt\n\n# Single field descending\nGET /api/v1/rooms?sort=-createdAt\n\n# Multiple fields\nGET /api/v1/rooms?sort=priority,-createdAt,name\n```\n\n| Prefix | Direction |\n|--------|-----------|\n| (none) | Ascending |\n| `-` | Descending |\n\n### Legacy Format\n\nThe original `sort` + `order` format is still supported for backwards compatibility:\n\n```bash\nGET /api/v1/rooms?sort=name&order=desc\n```\n\n| Parameter | Values | Default | Description |\n|-----------|--------|---------|-------------|\n| `sort` | Any column name | `createdAt` | Field to sort by |\n| `order` | `asc`, `desc` | `desc` | Sort direction |\n\nWhen the multi-sort format is detected (comma or `-` prefix), the `order` parameter is ignored.\n\n## Field Selection\n\nSelect which columns to return using `?fields=`. Available on LIST and GET routes (not Views — they define their own field set).\n\n```bash\n# Return only id, name, and status\nGET /api/v1/rooms?fields=id,name,status\n\n# Combine with other query params\nGET /api/v1/rooms?fields=id,name,status&status=active&sort=-createdAt\n\n# Single record\nGET /api/v1/rooms/rm_123?fields=id,name,capacity\n```\n\nAll columns are available including system columns (`id`, `organizationId`, `createdAt`, `modifiedAt`, etc.). Invalid field names are silently ignored. If no valid fields are provided, all columns are returned.\n\n**Security**: Masking still applies to selected fields. Requesting `?fields=ssn` will return the masked value, not the raw data.\n\n## Total Count\n\nGet the total number of matching records across all pages by adding `?total=true`. Available on LIST and VIEW routes.\n\n```bash\nGET /api/v1/rooms?status=active&total=true\n```\n\n```json\n{\n \"data\": [ /* 25 records */ ],\n \"pagination\": {\n \"limit\": 25,\n \"offset\": 0,\n \"count\": 25,\n \"total\": 142\n }\n}\n```\n\nThis is opt-in because it runs an additional `COUNT(*)` query. Only use it when you need the total (e.g., for pagination UI).\n\n## Search\n\nFull-text search across all text columns using `?search=`. Available on LIST and VIEW routes.\n\n```bash\n# Search across all text fields\nGET /api/v1/rooms?search=conference\n\n# Combine with filters\nGET /api/v1/rooms?search=conference&status=active\n```\n\nThe search generates an OR'd `LIKE` condition across all `text()` columns in your schema:\n\n```sql\nWHERE (name LIKE '%conference%' OR description LIKE '%conference%')\n```\n\nOnly columns defined with `text()` in your Drizzle schema are searchable. Non-text columns (integers, timestamps, UUIDs, blobs) are automatically excluded.\n\n## Complete Example\n\nCombine all query parameters together:\n\n```bash\nGET /api/v1/rooms?fields=id,name,status,capacity&status=active&capacity.gte=10&search=conference&sort=capacity,-createdAt&limit=10&offset=20&total=true\n```\n\nThis request:\n1. **Selects** only `id`, `name`, `status`, `capacity` fields\n2. **Filters** to active rooms with capacity >= 10\n3. **Searches** text columns for \"conference\"\n4. **Sorts** by capacity ascending, then createdAt descending\n5. **Paginates** with 10 results starting at offset 20\n6. **Counts** total matching records\n\n## Parameter Summary\n\n| Parameter | Applies To | Description |\n|-----------|-----------|-------------|\n| `limit` | LIST, VIEW | Page size (default: 50, max: 100) |\n| `offset` | LIST, VIEW | Skip N records |\n| `sort` | LIST, VIEW | Sort fields (comma-separated, `-` prefix for desc) |\n| `order` | LIST, VIEW | Legacy sort direction (`asc` or `desc`) |\n| `fields` | LIST, GET | Comma-separated column names to return |\n| `total` | LIST, VIEW | Set to `true` to include total count |\n| `search` | LIST, VIEW | Search text across all text columns |\n| `field=value` | LIST, VIEW | Filter by exact match |\n| `field.op=value` | LIST, VIEW | Filter with operator (gt, gte, lt, lte, ne, like, in) |"
270
+ "content": "The generated API supports filtering, pagination, sorting, field selection, search, and total count via query parameters on `GET` list endpoints.\n\n## Filter Operators\n\n| Operator | Query Param | SQL Equivalent |\n|----------|-------------|----------------|\n| Equals | `?field=value` | `WHERE field = value` |\n| Not equals | `?field.ne=value` | `WHERE field != value` |\n| Greater than | `?field.gt=value` | `WHERE field > value` |\n| Greater or equal | `?field.gte=value` | `WHERE field >= value` |\n| Less than | `?field.lt=value` | `WHERE field < value` |\n| Less or equal | `?field.lte=value` | `WHERE field <= value` |\n| Pattern match | `?field.like=value` | `WHERE field LIKE '%value%'` |\n| In list | `?field.in=a,b,c` | `WHERE field IN ('a','b','c')` |\n\n### Examples\n\n```bash\n# Filter by status\nGET /api/v1/jobs?status=open\n\n# Range query\nGET /api/v1/jobs?salaryMin.gte=100000&salaryMax.lte=200000\n\n# Pattern matching\nGET /api/v1/jobs?title.like=Engineer\n\n# Multiple values\nGET /api/v1/jobs?department.in=Engineering,Design,Product\n```\n\n## Pagination\n\n| Parameter | Default | Description |\n|-----------|---------|-------------|\n| `limit` | `50` | Number of records to return (min: 1, max: 100) |\n| `offset` | `0` | Number of records to skip |\n\n```bash\nGET /api/v1/jobs?limit=25&offset=50\n```\n\nThe default `limit` can be configured per-resource in your definition:\n\n```typescript\ncrud: {\n list: {\n access: { roles: [\"recruiter\"] },\n pageSize: 25, // Default limit\n maxPageSize: 100, // Maximum allowed limit\n },\n},\n```\n\n### Response Shape\n\n```json\n{\n \"data\": [ /* records */ ],\n \"pagination\": {\n \"limit\": 25,\n \"offset\": 50,\n \"count\": 12\n }\n}\n```\n\n- `count` — number of records returned on this page\n- `total` — total matching records across all pages (only when `?total=true`, see below)\n\n## Sorting\n\nSort by one or more fields. Use the `-` prefix for descending order.\n\n### Multi-Sort (Recommended)\n\n```bash\n# Sort by status ascending, then createdAt descending\nGET /api/v1/jobs?sort=status,-createdAt\n\n# Single field descending\nGET /api/v1/jobs?sort=-createdAt\n\n# Multiple fields\nGET /api/v1/jobs?sort=department,-salaryMax,title\n```\n\n| Prefix | Direction |\n|--------|-----------|\n| (none) | Ascending |\n| `-` | Descending |\n\n### Legacy Format\n\nThe original `sort` + `order` format is still supported for backwards compatibility:\n\n```bash\nGET /api/v1/jobs?sort=title&order=asc\n```\n\n| Parameter | Values | Default | Description |\n|-----------|--------|---------|-------------|\n| `sort` | Any column name | `createdAt` | Field to sort by |\n| `order` | `asc`, `desc` | `desc` | Sort direction |\n\nWhen the multi-sort format is detected (comma or `-` prefix), the `order` parameter is ignored.\n\n## Field Selection\n\nSelect which columns to return using `?fields=`. Available on LIST and GET routes (not Views — they define their own field set).\n\n```bash\n# Return only id, title, and status\nGET /api/v1/jobs?fields=id,title,status\n\n# Combine with other query params\nGET /api/v1/jobs?fields=id,title,status&status=open&sort=-createdAt\n\n# Single record\nGET /api/v1/jobs/job_123?fields=id,title,salaryMin,salaryMax\n```\n\nAll columns are available including system columns (`id`, `organizationId`, `createdAt`, `modifiedAt`, etc.). Invalid field names are silently ignored. If no valid fields are provided, all columns are returned.\n\n**Security**: Masking still applies to selected fields. Requesting `?fields=ssn` will return the masked value, not the raw data.\n\n## Total Count\n\nGet the total number of matching records across all pages by adding `?total=true`. Available on LIST and VIEW routes.\n\n```bash\nGET /api/v1/jobs?status=open&total=true\n```\n\n```json\n{\n \"data\": [ /* 25 records */ ],\n \"pagination\": {\n \"limit\": 25,\n \"offset\": 0,\n \"count\": 25,\n \"total\": 142\n }\n}\n```\n\nThis is opt-in because it runs an additional `COUNT(*)` query. Only use it when you need the total (e.g., for pagination UI).\n\n## Search\n\nFull-text search across all text columns using `?search=`. Available on LIST and VIEW routes.\n\n```bash\n# Search across all text fields\nGET /api/v1/jobs?search=engineer\n\n# Combine with filters\nGET /api/v1/jobs?search=engineer&status=open\n```\n\nThe search generates an OR'd `LIKE` condition across all `text()` columns in your schema:\n\n```sql\nWHERE (title LIKE '%engineer%' OR department LIKE '%engineer%')\n```\n\nOnly columns defined with `text()` in your Drizzle schema are searchable. Non-text columns (integers, timestamps, UUIDs, blobs) are automatically excluded.\n\n## Complete Example\n\nCombine all query parameters together:\n\n```bash\nGET /api/v1/jobs?fields=id,title,status,salaryMin&status=open&salaryMin.gte=100000&search=engineer&sort=salaryMin,-createdAt&limit=10&offset=20&total=true\n```\n\nThis request:\n1. **Selects** only `id`, `title`, `status`, `salaryMin` fields\n2. **Filters** to open jobs with salaryMin >= 100000\n3. **Searches** text columns for \"engineer\"\n4. **Sorts** by salaryMin ascending, then createdAt descending\n5. **Paginates** with 10 results starting at offset 20\n6. **Counts** total matching records\n\n## Parameter Summary\n\n| Parameter | Applies To | Description |\n|-----------|-----------|-------------|\n| `limit` | LIST, VIEW | Page size (default: 50, max: 100) |\n| `offset` | LIST, VIEW | Skip N records |\n| `sort` | LIST, VIEW | Sort fields (comma-separated, `-` prefix for desc) |\n| `order` | LIST, VIEW | Legacy sort direction (`asc` or `desc`) |\n| `fields` | LIST, GET | Comma-separated column names to return |\n| `total` | LIST, VIEW | Set to `true` to include total count |\n| `search` | LIST, VIEW | Search text across all text columns |\n| `field=value` | LIST, VIEW | Filter by exact match |\n| `field.op=value` | LIST, VIEW | Filter with operator (gt, gte, lt, lte, ne, like, in) |"
259
271
  },
260
272
  "compiler/using-the-api/views-api": {
261
273
  "title": "Views API",
262
- "content": "Views provide named column projections that limit which fields are returned in API responses. Each view can have its own access control, and all security pillars (firewall, masking) still apply.\n\n## Endpoint\n\n```\nGET /api/v1/{resource}/views/{view-name}\n```\n\n## Example\n\nGiven this definition:\n\n```typescript\nexport default defineTable(customers, {\n views: {\n summary: {\n fields: ['id', 'name', 'status'],\n access: { roles: ['owner', 'admin', 'member'] },\n },\n full: {\n fields: ['id', 'name', 'email', 'status', 'ssn', 'internalNotes'],\n access: { roles: ['owner', 'admin'] },\n },\n },\n // ...\n});\n```\n\n### Request\n\n```bash\ncurl /api/v1/customers/views/summary \\\n -H \"Authorization: Bearer <token>\"\n```\n\n### Response\n\n```json\n{\n \"data\": [\n { \"id\": \"cust_001\", \"name\": \"Acme Corp\", \"status\": \"active\" },\n { \"id\": \"cust_002\", \"name\": \"Widget Inc\", \"status\": \"pending\" }\n ],\n \"view\": \"summary\",\n \"fields\": [\"id\", \"name\", \"status\"],\n \"pagination\": {\n \"limit\": 50,\n \"offset\": 0,\n \"count\": 2\n }\n}\n```\n\nOnly the fields specified in the view definition are returned. Requesting the `full` view with a `member` role returns 403.\n\n## Query Parameters\n\nViews support the same query parameters as the list endpoint:\n\n| Parameter | Description | Default |\n|-----------|-------------|---------|\n| `limit` | Number of records to return (1–100) | `50` |\n| `offset` | Number of records to skip | `0` |\n| `sort` | Field to sort by | `createdAt` |\n| `order` | Sort direction (`asc` or `desc`) | `desc` |\n| `{field}` | Filter by exact value | — |\n| `{field}.gt` | Greater than | — |\n| `{field}.gte` | Greater than or equal | — |\n| `{field}.lt` | Less than | — |\n| `{field}.lte` | Less than or equal | — |\n| `{field}.ne` | Not equal | — |\n| `{field}.like` | Pattern match (SQL LIKE) | — |\n| `{field}.in` | Match any value in comma-separated list | — |\n\n### Examples\n\n```bash\n# Paginated with sorting\nGET /api/v1/customers/views/summary?limit=10&offset=0&sort=name&order=asc\n\n# With filters\nGET /api/v1/customers/views/summary?status=active&name.like=%25Corp%25\n\n# Combined\nGET /api/v1/customers/views/summary?status.in=active,pending&sort=name&order=asc&limit=25\n```\n\n## Security\n\nAll security pillars apply to view endpoints:\n\n1. **Authentication** — Required (401 if missing)\n2. **Firewall** — Organization/user isolation applied to all results. Only records the user owns or has access to are returned.\n3. **Access** — Per-view role check. Each view can require different roles.\n4. **Masking** — Applied to all returned fields. If a masked field is in the view's field list, the masking rules still apply based on the user's role.\n\n### Access Control Per View\n\nDifferent views can require different permission levels:\n\n```typescript\nviews: {\n // Available to all org members\n summary: {\n fields: ['id', 'name', 'status'],\n access: { roles: ['owner', 'admin', 'member'] },\n },\n // Admin-only view with sensitive data\n full: {\n fields: ['id', 'name', 'email', 'ssn', 'internalNotes'],\n access: { roles: ['owner', 'admin'] },\n },\n}\n```\n\nA `member` requesting the `full` view receives:\n\n```json\n{\n \"error\": \"Insufficient permissions\",\n \"layer\": \"access\",\n \"code\": \"ACCESS_ROLE_REQUIRED\",\n \"details\": { \"required\": [\"admin\"], \"current\": [\"member\"] }\n}\n```\n\n### Masking in Views\n\nMasking is applied after field projection. If your masking config hides `ssn` from non-admins:\n\n```typescript\nmasking: {\n ssn: { type: 'ssn', show: { roles: ['admin'] } },\n}\n```\n\nA `member` requesting a view that includes `ssn` will see the masked value (e.g., `*****1234`), while an `admin` sees the full value.\n\n## See Also\n\n- [Defining Views](/compiler/definitions/views) — How to configure views in defineTable\n- [Query Parameters](/compiler/using-the-api/query-params) — Full query parameter reference\n- [Access Control](/compiler/definitions/access) — Role-based access configuration"
274
+ "content": "Views provide named column projections that limit which fields are returned in API responses. Each view can have its own access control, and all security pillars (firewall, masking) still apply.\n\n## Endpoint\n\n```\nGET /api/v1/{resource}/views/{view-name}\n```\n\n## Example\n\nGiven this definition:\n\n```typescript\nexport default defineTable(candidates, {\n views: {\n pipeline: {\n fields: ['id', 'name', 'source'],\n access: { roles: ['owner', 'hiring-manager', 'recruiter', 'interviewer'] },\n },\n full: {\n fields: ['id', 'name', 'email', 'phone', 'resumeUrl', 'source'],\n access: { roles: ['owner', 'hiring-manager', 'recruiter'] },\n },\n },\n // ...\n});\n```\n\n### Request\n\n```bash\ncurl /api/v1/candidates/views/pipeline \\\n -H \"Authorization: Bearer <token>\"\n```\n\n### Response\n\n```json\n{\n \"data\": [\n { \"id\": \"cand_001\", \"name\": \"Alice Johnson\", \"source\": \"LinkedIn\" },\n { \"id\": \"cand_002\", \"name\": \"Bob Martinez\", \"source\": \"referral\" }\n ],\n \"view\": \"pipeline\",\n \"fields\": [\"id\", \"name\", \"source\"],\n \"pagination\": {\n \"limit\": 50,\n \"offset\": 0,\n \"count\": 2\n }\n}\n```\n\nOnly the fields specified in the view definition are returned. Requesting the `full` view with an `interviewer` role returns 403.\n\n## Query Parameters\n\nViews support the same query parameters as the list endpoint:\n\n| Parameter | Description | Default |\n|-----------|-------------|---------|\n| `limit` | Number of records to return (1–100) | `50` |\n| `offset` | Number of records to skip | `0` |\n| `sort` | Field to sort by | `createdAt` |\n| `order` | Sort direction (`asc` or `desc`) | `desc` |\n| `{field}` | Filter by exact value | — |\n| `{field}.gt` | Greater than | — |\n| `{field}.gte` | Greater than or equal | — |\n| `{field}.lt` | Less than | — |\n| `{field}.lte` | Less than or equal | — |\n| `{field}.ne` | Not equal | — |\n| `{field}.like` | Pattern match (SQL LIKE) | — |\n| `{field}.in` | Match any value in comma-separated list | — |\n\n### Examples\n\n```bash\n# Paginated with sorting\nGET /api/v1/candidates/views/pipeline?limit=10&offset=0&sort=name&order=asc\n\n# With filters\nGET /api/v1/candidates/views/pipeline?source=referral&name.like=%25Johnson%25\n\n# Combined\nGET /api/v1/candidates/views/pipeline?source.in=LinkedIn,referral&sort=name&order=asc&limit=25\n```\n\n## Security\n\nAll security pillars apply to view endpoints:\n\n1. **Authentication** — Required (401 if missing)\n2. **Firewall** — Organization/user isolation applied to all results. Only records the user owns or has access to are returned.\n3. **Access** — Per-view role check. Each view can require different roles.\n4. **Masking** — Applied to all returned fields. If a masked field is in the view's field list, the masking rules still apply based on the user's role.\n\n### Access Control Per View\n\nDifferent views can require different permission levels:\n\n```typescript\nviews: {\n // Available to all org members including interviewers\n pipeline: {\n fields: ['id', 'name', 'source'],\n access: { roles: ['owner', 'hiring-manager', 'recruiter', 'interviewer'] },\n },\n // Restricted view with contact details\n full: {\n fields: ['id', 'name', 'email', 'phone', 'resumeUrl', 'source'],\n access: { roles: ['owner', 'hiring-manager', 'recruiter'] },\n },\n}\n```\n\nAn `interviewer` requesting the `full` view receives:\n\n```json\n{\n \"error\": \"Insufficient permissions\",\n \"layer\": \"access\",\n \"code\": \"ACCESS_ROLE_REQUIRED\",\n \"details\": { \"required\": [\"hiring-manager\", \"recruiter\"], \"current\": [\"interviewer\"] }\n}\n```\n\n### Masking in Views\n\nMasking is applied after field projection. If your masking config hides `phone` from interviewers:\n\n```typescript\nmasking: {\n phone: { type: 'phone', show: { roles: ['hiring-manager', 'recruiter'] } },\n}\n```\n\nAn `interviewer` requesting a view that includes `phone` will see the masked value (e.g., `***-***-5678`), while a `recruiter` sees the full value.\n\n## See Also\n\n- [Defining Views](/compiler/definitions/views) — How to configure views in defineTable\n- [Query Parameters](/compiler/using-the-api/query-params) — Full query parameter reference\n- [Access Control](/compiler/definitions/access) — Role-based access configuration"
263
275
  },
264
276
  "index": {
265
277
  "title": "Quickback Documentation",
266
- "content": "# Quickback Documentation\n\nWelcome to the Quickback documentation. Quickback is a **backend compiler** that transforms declarative resource definitions into a fully-featured, production-ready API with built-in security layers.\n\n## Products\n\n### [Quickback Compiler](/compiler)\n\nThe core product. Define your database schema and security rules in TypeScript, then compile them into a complete backend.\n\n- **[Getting Started](/compiler/getting-started)** — Create your first project\n- **[Definitions](/compiler/definitions)** — Schema, Firewall, Access, Guards, Masking, Views, Actions\n- **[Using the API](/compiler/using-the-api)** — CRUD, filtering, batch operations\n- **[Cloud Compiler](/compiler/cloud-compiler)** — CLI, authentication, endpoints\n\n---\n\n### [Quickback Stack](/stack)\n\nProduction-ready Cloudflare + Better Auth infrastructure that runs on YOUR Cloudflare account.\n\n- **[Auth](/stack/auth)** — Better Auth plugins, security, device auth, API keys\n- **[Database](/stack/database)** — D1, Neon\n- **[Storage](/stack/storage)** — KV, R2\n- **[Realtime](/stack/realtime)** — Durable Objects + WebSocket\n\n---\n\n### [Account UI](/account-ui)\n\nPre-built authentication and account management UI. Deploy to Cloudflare Workers.\n\n- **[Features](/account-ui)** — Sessions, organizations, passkeys, admin panel\n- **[Customization](/account-ui/customization)** — Branding, theming, feature flags\n\n---\n\n### [Plugins & Tools](/plugins-tools)\n\nOpen-source Better Auth plugins and developer tools.\n\n- **[Better Auth Plugins](/plugins-tools)** — AWS SES, combo auth, upgrade anonymous\n- **[Claude Code Skill](/plugins-tools/claude-code-skill)** — AI-powered Quickback assistance\n\n### [Changelog](/changelog)\n\nWhat's new in Quickback — release notes, new features, and improvements.\n\n---\n\n## Quick Start\n\n```bash\nnpm install -g @kardoe/quickback\nquickback create cloudflare my-app\ncd my-app\nquickback compile\n```\n\n## How It Works\n\n1. **Define** your database schema using Drizzle ORM\n2. **Configure** security layers (firewall, access, guards, masking) for each resource\n3. **Compile** your definitions into a deployable backend\n4. **Deploy** to Cloudflare Workers or your own infrastructure\n\n## Security Philosophy: Locked Down by Default\n\nQuickback is **secure by default**. Nothing is accessible until you explicitly open it up. Learn more in [Definitions](/compiler/definitions)."
278
+ "content": "# Quickback Documentation\n\nQuickback is a **backend compiler**. Define your database schema and security rules in TypeScript — Quickback compiles them into a complete, production-ready API.\n\n## Quick Start\n\n```bash\nnpm install -g @kardoe/quickback\nquickback create cloudflare my-app\ncd my-app\nquickback compile\n```\n\n## How It Works\n\nWrite ~50 lines of configuration. Get 500+ lines of production code.\n\n```\ndefineTable() → Compiler → Production API\n │ │\n ├── Schema ├── CRUD routes + batch operations\n ├── Firewall ├── Auth middleware\n ├── Access ├── Data isolation queries\n ├── Guards ├── Field validation\n └── Masking ├── PII redaction\n ├── TypeScript types\n └── Database migrations\n```\n\nEvery API request passes through four security layers:\n\n```\nRequest → Firewall → Access → Guards → Masking → Response\n │ │ │ │\n │ │ │ └── Hide sensitive fields\n │ │ └── Block field modifications\n │ └── Check roles & conditions\n └── Isolate data by owner/org\n```\n\n**Secure by default.** Nothing is accessible until you explicitly open it up. See [Definitions](/compiler/definitions) for the full security model.\n\n---\n\n## Products\n\n### [Quickback Compiler](/compiler)\n\nThe core product. Define your database schema and security rules in TypeScript, then compile them into a complete backend.\n\n- **[Getting Started](/compiler/getting-started)** — Create your first project\n- **[Definitions](/compiler/definitions)** — Schema, Firewall, Access, Guards, Masking, Views, Actions\n- **[Using the API](/compiler/using-the-api)** — CRUD, filtering, batch operations\n- **[Cloud Compiler](/compiler/cloud-compiler)** — CLI, authentication, endpoints\n\n---\n\n### [Quickback Stack](/stack)\n\nProduction-ready Cloudflare + Better Auth infrastructure that runs on YOUR Cloudflare account.\n\n- **[Auth](/stack/auth)** — Better Auth plugins, security, device auth, API keys\n- **[Database](/stack/database)** — D1, Neon\n- **[Storage](/stack/storage)** — KV, R2\n- **[Realtime](/stack/realtime)** — Durable Objects + WebSocket\n\n---\n\n### [Account UI](/account-ui)\n\nPre-built authentication and account management UI. Deploy to Cloudflare Workers.\n\n- **[Features](/account-ui)** — Sessions, organizations, passkeys, admin panel\n- **[Customization](/account-ui/customization)** — Branding, theming, feature flags\n\n---\n\n### [Plugins & Tools](/plugins-tools)\n\nOpen-source Better Auth plugins and developer tools.\n\n- **[Better Auth Plugins](/plugins-tools)** — AWS SES, combo auth, upgrade anonymous\n- **[Claude Code Skill](/plugins-tools/claude-code-skill)** — AI-powered Quickback assistance\n\n### [Changelog](/changelog)\n\nWhat's new in Quickback — release notes, new features, and improvements."
267
279
  },
268
280
  "plugins-tools/better-auth-plugins/aws-ses": {
269
281
  "title": "AWS SES Plugin",
@@ -271,7 +283,7 @@ export const DOCS = {
271
283
  },
272
284
  "plugins-tools/better-auth-plugins/combo-auth": {
273
285
  "title": "Combo Auth Plugin",
274
- "content": "`@kardoe/better-auth-combo-auth` provides a combined authentication flow that sends both a magic link and an OTP code in a single email.\n\n## Installation\n\n```bash\nnpm install @kardoe/better-auth-combo-auth\n```\n\n## How It Works\n\nWhen a user requests authentication:\n1. A single email is sent with both a magic link and a 6-digit OTP\n2. The user can click the link (one-click) or enter the OTP code\n3. Either method authenticates the user\n\nThis provides the best of both worlds convenience of magic links with the fallback of OTP codes."
286
+ "content": "`@kardoe/better-auth-combo-auth` provides a combined authentication flow that sends both a magic link and an OTP code in a single email. Users can click the link (one-click) or enter the 6-digit OTP code — whichever is more convenient.\n\n## Installation\n\n```bash\nnpm install @kardoe/better-auth-combo-auth\n```\n\n## Server Setup\n\nAdd the plugin to your Better Auth configuration:\n\n```typescript\n\nexport const auth = betterAuth({\n plugins: [\n comboAuth({\n sendComboAuth: async ({ email, otp, url }) => {\n // Send email with both the magic link (url) and OTP code (otp)\n await sendEmail({\n to: email,\n subject: \"Sign in to My App\",\n html: `\n <p>Click the link below to sign in:</p>\n <a href=\"${url}\">Sign in to My App</a>\n <p>Or enter this code: <strong>${otp}</strong></p>\n `,\n });\n },\n }),\n ],\n});\n```\n\n## Client Usage\n\n```typescript\n\nconst authClient = createAuthClient({\n plugins: [comboAuthClient()],\n});\n\n// Request authentication — sends email with link + OTP\nawait authClient.signIn.comboAuth({\n email: \"user@example.com\",\n});\n\n// Verify with OTP code (if user enters it manually)\nawait authClient.signIn.comboAuth({\n email: \"user@example.com\",\n otp: \"123456\",\n});\n```\n\n## How It Works\n\n1. User enters their email and requests sign-in\n2. A single email is sent containing both a magic link and a 6-digit OTP\n3. **Option A:** User clicks the magic link authenticated immediately\n4. **Option B:** User enters the OTP code authenticated after verification\n\nThis provides the best of both worlds: convenience of magic links with the reliability of OTP codes (useful when magic links don't work well, e.g., opening email on a different device).\n\n## See Also\n\n- [Auth Plugins](/stack/auth/plugins) — All authentication plugin options\n- [Upgrade Anonymous](/plugins-tools/better-auth-plugins/upgrade-anonymous) — Convert anonymous sessions to full accounts\n- [AWS SES](/plugins-tools/better-auth-plugins/aws-ses) — Email delivery for auth flows"
275
287
  },
276
288
  "plugins-tools/better-auth-plugins/upgrade-anonymous": {
277
289
  "title": "Upgrade Anonymous Plugin",
@@ -279,7 +291,7 @@ export const DOCS = {
279
291
  },
280
292
  "plugins-tools/claude-code-skill": {
281
293
  "title": "Claude Code Integration",
282
- "content": "Build Quickback apps faster with Claude Code. The Quickback skill gives Claude deep knowledge of security layers, patterns, and best practices—so you can describe what you want and let Claude write the configuration.\n\n## Installation\n\n### Option 1: Install globally (Recommended)\n\nInstall the skill once and use it across all your projects:\n\n```bash\nnpm install -g @kardoe/quickback-skill\n```\n\nThis installs to `~/.claude/skills/quickback/` and is automatically available in Claude Code.\n\n### Option 2: New Quickback project\n\nCreate a new Quickback project—the skill is included automatically:\n\n```bash\nnpx @kardoe/quickback create cloudflare my-app\ncd my-app\n```\n\n### Option 3: Manual installation\n\nDownload the skill directly:\n\n```bash\nmkdir -p ~/.claude/skills/quickback\ncurl -o ~/.claude/skills/quickback/SKILL.md \\\n https://raw.githubusercontent.com/kardoe/quickback/main/.claude/skills/quickback/SKILL.md\n```\n\n## What You Get\n\nWhen you install the Quickback skill, you get:\n\n### Quickback Skill\n\nClaude understands Quickback concepts and can answer questions about:\n\n- **Security layers** - Firewall, Access, Guards, Masking, Actions\n- **Common patterns** - Multi-tenant, owner-scoped, hierarchical access\n- **Best practices** - Field protection, role-based access, PII handling\n- **Database dialects** - SQLite/D1, PostgreSQL, MySQL syntax\n\n### Quickback Specialist Agent\n\nClaude also gets a specialized agent that activates automatically when you're:\n\n- Creating new resources with schemas and security configurations\n- Configuring security layers (Firewall, Access, Guards, Masking)\n- Defining actions for business logic\n- Debugging configuration issues\n\nThe agent generates complete, working code in your `definitions/features/` directory.\n\n## Usage\n\n### Let Claude help automatically\n\nJust describe what you need. Claude will use Quickback knowledge when relevant:\n\n```\n\"Create a tasks resource where users can only see their own tasks,\nbut admins can see all tasks in the organization\"\n```\n\n### Invoke directly\n\nUse `/quickback` to explicitly activate the skill:\n\n```\n/quickback How do I configure soft delete?\n```\n\n## Example Conversations\n\n### Building a New Resource\n\n**You:** \"I need a resource for invoices. Users should only see invoices from their organization. The status field should only be changeable through approve/reject actions. Mask the customer email for non-admins.\"\n\n**Claude:** Creates complete configuration with:\n- Firewall scoped to organization\n- Protected status field with approve/reject actions\n- Email masking with admin bypass\n- Appropriate guards for createable/updatable fields\n\n### Understanding Existing Code\n\n**You:** \"Explain what this firewall configuration does\"\n\n**Claude:** Breaks down the WHERE clauses, explains the security implications, and identifies potential issues.\n\n### Debugging Configuration\n\n**You:** \"My users can see records from other organizations. What's wrong?\"\n\n**Claude:** Analyzes your firewall setup, checks for `exception: true` or missing organization scope, and suggests fixes.\n\n## What Claude Knows\n\n### Security Layers\n\n| Layer | What Claude Helps With |\n|-------|------------------------|\n| **Firewall** | Data isolation patterns, WHERE clause generation, soft delete |\n| **Access** | Role-based permissions, record-level conditions, combining rules |\n| **Guards** | Field protection, createable vs updatable, immutable fields |\n| **Masking** | PII redaction, role-based visibility, mask types |\n| **Actions** | Custom endpoints, protected field updates, input validation |\n\n### Common Patterns\n\nClaude recognizes and can implement these patterns:\n\n- **Multi-tenant SaaS** - Organization-scoped with role hierarchy\n- **Personal data apps** - Owner-scoped resources\n- **Hierarchical access** - Admins see all, users see own\n- **Public resources** - Reference data, system tables\n- **Workflow resources** - Status fields with action-based transitions\n\n### Database Support\n\nClaude generates correct syntax for your database:\n\n```typescript\n// SQLite / Cloudflare D1\ncreatedAt: integer('created_at', { mode: 'timestamp' })\n\n// PostgreSQL / Supabase\ncreatedAt: timestamp('created_at').defaultNow()\n\n// MySQL\ncreatedAt: timestamp('created_at')\n```\n\n## Tips for Best Results\n\n**Be specific about security requirements:**\n\n```\n// Good\n\"Users can only see their own tasks. Admins can see all tasks\nin the organization. The priority field can only be set by admins.\"\n\n// Less helpful\n\"Create a tasks resource\"\n```\n\n**Describe your user types:**\n\n```\n\"We have three roles: admin (full access), manager (can approve),\nand member (can only edit their own records)\"\n```\n\n**Mention sensitive fields:**\n\n```\n\"The ssn field should be masked for everyone except HR admins\"\n```\n\n## Common Tasks\n\n### Create a complete resource\n\n```\n\"Create an employees resource for a multi-tenant HR app. Include\nfields for name, email, department, salary. Mask salary for\nnon-admins. Only HR can create/delete employees.\"\n```\n\n### Add an action to existing resource\n\n```\n\"Add an 'approve' action to the expenses resource that sets\nstatus to 'approved' and records the approver\"\n```\n\n### Configure access control\n\n```\n\"Update the projects resource so managers can edit any project\nin their organization, but members can only edit projects they created\"\n```\n\n### Set up masking\n\n```\n\"Add masking to the customers resource: email partially masked,\nphone last 4 digits only, SSN fully redacted except for finance role\"\n```\n\n## Troubleshooting\n\n### Skill not found\n\nVerify the skill is installed:\n\n```bash\nls ~/.claude/skills/quickback/SKILL.md\n```\n\nIf missing, reinstall:\n\n```bash\nnpm install -g @kardoe/quickback-skill\n```\n\n### Claude doesn't understand Quickback\n\nMake sure the skill file exists and Claude Code is restarted. You can also invoke it directly with `/quickback` to force it to load.\n\n### Generated code has errors\n\nRun `quickback compile` to validate. Share the error messages with Claude for fixes.\n\n## Updating the Skill\n\nTo get the latest version:\n\n```bash\nnpm update -g @kardoe/quickback-skill\n```\n\n## Resources\n\n- [Getting Started](/compiler/getting-started) - Get your first Quickback project running\n- [Definitions Overview](/compiler/definitions) - Understand the security layer model\n- [npm package](https://www.npmjs.com/package/@kardoe/quickback-skill) - Skill package\n\n## Feedback\n\nFound an issue with the Claude Code integration?\n\n- [GitHub Issues](https://github.com/kardoe/quickback/issues)\n- [Quickback Documentation](https://docs.quickback.dev)"
294
+ "content": "Build Quickback apps faster with Claude Code. The Quickback skill gives Claude deep knowledge of security layers, patterns, and best practices—so you can describe what you want and let Claude write the configuration.\n\n## Installation\n\n### Option 1: Install globally (Recommended)\n\nInstall the skill once and use it across all your projects:\n\n```bash\nnpm install -g @kardoe/quickback-skill\n```\n\nThis installs to `~/.claude/skills/quickback/` and is automatically available in Claude Code.\n\n### Option 2: New Quickback project\n\nCreate a new Quickback project—the skill is included automatically:\n\n```bash\nnpx @kardoe/quickback create cloudflare my-app\ncd my-app\n```\n\n### Option 3: Manual installation\n\nDownload the skill directly:\n\n```bash\nmkdir -p ~/.claude/skills/quickback\ncurl -o ~/.claude/skills/quickback/SKILL.md \\\n https://raw.githubusercontent.com/kardoe/quickback/main/.claude/skills/quickback/SKILL.md\n```\n\n## What You Get\n\nWhen you install the Quickback skill, you get:\n\n### Quickback Skill\n\nClaude understands Quickback concepts and can answer questions about:\n\n- **Security layers** - Firewall, Access, Guards, Masking, Actions\n- **Common patterns** - Multi-tenant, owner-scoped, hierarchical access\n- **Best practices** - Field protection, role-based access, PII handling\n- **Database dialects** - SQLite/D1, PostgreSQL, MySQL syntax\n\n### Quickback Specialist Agent\n\nClaude also gets a specialized agent that activates automatically when you're:\n\n- Creating new resources with schemas and security configurations\n- Configuring security layers (Firewall, Access, Guards, Masking)\n- Defining actions for business logic\n- Debugging configuration issues\n\nThe agent generates complete, working code in your `quickback/features/` directory.\n\n## Usage\n\n### Let Claude help automatically\n\nJust describe what you need. Claude will use Quickback knowledge when relevant:\n\n```\n\"Create a tasks resource where users can only see their own tasks,\nbut admins can see all tasks in the organization\"\n```\n\n### Invoke directly\n\nUse `/quickback` to explicitly activate the skill:\n\n```\n/quickback How do I configure soft delete?\n```\n\n## Example Conversations\n\n### Building a New Resource\n\n**You:** \"I need a resource for invoices. Users should only see invoices from their organization. The status field should only be changeable through approve/reject actions. Mask the customer email for non-admins.\"\n\n**Claude:** Creates complete configuration with:\n- Firewall scoped to organization\n- Protected status field with approve/reject actions\n- Email masking with admin bypass\n- Appropriate guards for createable/updatable fields\n\n### Understanding Existing Code\n\n**You:** \"Explain what this firewall configuration does\"\n\n**Claude:** Breaks down the WHERE clauses, explains the security implications, and identifies potential issues.\n\n### Debugging Configuration\n\n**You:** \"My users can see records from other organizations. What's wrong?\"\n\n**Claude:** Analyzes your firewall setup, checks for `exception: true` or missing organization scope, and suggests fixes.\n\n## What Claude Knows\n\n### Security Layers\n\n| Layer | What Claude Helps With |\n|-------|------------------------|\n| **Firewall** | Data isolation patterns, WHERE clause generation, soft delete |\n| **Access** | Role-based permissions, record-level conditions, combining rules |\n| **Guards** | Field protection, createable vs updatable, immutable fields |\n| **Masking** | PII redaction, role-based visibility, mask types |\n| **Actions** | Custom endpoints, protected field updates, input validation |\n\n### Common Patterns\n\nClaude recognizes and can implement these patterns:\n\n- **Multi-tenant SaaS** - Organization-scoped with role hierarchy\n- **Personal data apps** - Owner-scoped resources\n- **Hierarchical access** - Admins see all, users see own\n- **Public resources** - Reference data, system tables\n- **Workflow resources** - Status fields with action-based transitions\n\n### Database Support\n\nClaude generates correct syntax for your database:\n\n```typescript\n// SQLite / Cloudflare D1\ncreatedAt: integer('created_at', { mode: 'timestamp' })\n\n// PostgreSQL / Supabase\ncreatedAt: timestamp('created_at').defaultNow()\n\n// MySQL\ncreatedAt: timestamp('created_at')\n```\n\n## Tips for Best Results\n\n**Be specific about security requirements:**\n\n```\n// Good\n\"Users can only see their own tasks. Admins can see all tasks\nin the organization. The priority field can only be set by admins.\"\n\n// Less helpful\n\"Create a tasks resource\"\n```\n\n**Describe your user types:**\n\n```\n\"We have three roles: admin (full access), manager (can approve),\nand member (can only edit their own records)\"\n```\n\n**Mention sensitive fields:**\n\n```\n\"The ssn field should be masked for everyone except HR admins\"\n```\n\n## Common Tasks\n\n### Create a complete resource\n\n```\n\"Create an employees resource for a multi-tenant HR app. Include\nfields for name, email, department, salary. Mask salary for\nnon-admins. Only HR can create/delete employees.\"\n```\n\n### Add an action to existing resource\n\n```\n\"Add an 'approve' action to the expenses resource that sets\nstatus to 'approved' and records the approver\"\n```\n\n### Configure access control\n\n```\n\"Update the projects resource so managers can edit any project\nin their organization, but members can only edit projects they created\"\n```\n\n### Set up masking\n\n```\n\"Add masking to the customers resource: email partially masked,\nphone last 4 digits only, SSN fully redacted except for finance role\"\n```\n\n## Troubleshooting\n\n### Skill not found\n\nVerify the skill is installed:\n\n```bash\nls ~/.claude/skills/quickback/SKILL.md\n```\n\nIf missing, reinstall:\n\n```bash\nnpm install -g @kardoe/quickback-skill\n```\n\n### Claude doesn't understand Quickback\n\nMake sure the skill file exists and Claude Code is restarted. You can also invoke it directly with `/quickback` to force it to load.\n\n### Generated code has errors\n\nRun `quickback compile` to validate. Share the error messages with Claude for fixes.\n\n## Updating the Skill\n\nTo get the latest version:\n\n```bash\nnpm update -g @kardoe/quickback-skill\n```\n\n## Resources\n\n- [Getting Started](/compiler/getting-started) - Get your first Quickback project running\n- [Definitions Overview](/compiler/definitions) - Understand the security layer model\n- [npm package](https://www.npmjs.com/package/@kardoe/quickback-skill) - Skill package\n\n## Feedback\n\nFound an issue with the Claude Code integration?\n\n- [GitHub Issues](https://github.com/kardoe/quickback/issues)\n- [Quickback Documentation](https://docs.quickback.dev)"
283
295
  },
284
296
  "plugins-tools": {
285
297
  "title": "Plugins & Tools",
@@ -311,7 +323,7 @@ export const DOCS = {
311
323
  },
312
324
  "stack/database/d1": {
313
325
  "title": "D1 Database",
314
- "content": "Cloudflare D1 is SQLite at the edge. Quickback uses D1 as the primary database for Cloudflare deployments, with a multi-database pattern for separation of concerns.\n\n## What is D1?\n\nD1 is Cloudflare's serverless SQL database built on SQLite:\n\n- **Edge-native** - Runs in Cloudflare's global network\n- **SQLite compatible** - Use familiar SQL syntax\n- **Zero configuration** - No connection strings or pooling\n- **Automatic replication** - Read replicas at every edge location\n\n## Multi-Database Pattern\n\nQuickback generates separate D1 databases for different concerns:\n\n| Database | Binding | Purpose |\n|----------|---------|---------|\n| `AUTH_DB` | `AUTH_DB` | Better Auth tables (user, session, account) |\n| `DB` | `DB` | Your application data |\n| `FILES_DB` | `FILES_DB` | File metadata for R2 uploads |\n| `WEBHOOKS_DB` | `WEBHOOKS_DB` | Webhook delivery tracking |\n\nThis separation provides:\n- **Independent scaling** - Auth traffic doesn't affect app queries\n- **Isolation** - Auth schema changes don't touch your data\n- **Clarity** - Clear ownership of each database\n\n## Drizzle ORM Integration\n\nQuickback uses [Drizzle ORM](https://orm.drizzle.team/) for type-safe database access:\n\n```ts\n// schema/tables.ts - Your schema definition\n\nexport const posts = sqliteTable(\"posts\", {\n id: text(\"id\").primaryKey(),\n title: text(\"title\").notNull(),\n content: text(\"content\"),\n authorId: text(\"author_id\").notNull(),\n createdAt: integer(\"created_at\", { mode: \"timestamp\" }),\n});\n```\n\nThe compiler generates Drizzle queries based on your security rules:\n\n```ts\n// Generated query with firewall applied\nconst result = await db\n .select()\n .from(posts)\n .where(eq(posts.authorId, userId)); // Firewall injects ownership\n```\n\n## Migrations\n\nQuickback generates migrations automatically at compile time based on your schema changes:\n\n```bash\n# Compile your project (generates migrations)\nquickback compile\n\n# Apply migrations locally\nwrangler d1 migrations apply DB --local\n\n# Apply to production D1\nwrangler d1 migrations apply DB --remote\n```\n\nMigration files are generated in `drizzle/migrations/` and version-controlled with your code. You never need to manually generate migrations—just define your schema and compile.\n\n## wrangler.toml Bindings\n\nConfigure D1 bindings in `wrangler.toml`:\n\n```toml\n[[d1_databases]]\nbinding = \"DB\"\ndatabase_name = \"my-app-db\"\ndatabase_id = \"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\"\n\n[[d1_databases]]\nbinding = \"AUTH_DB\"\ndatabase_name = \"my-app-auth\"\ndatabase_id = \"yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy\"\n\n[[d1_databases]]\nbinding = \"FILES_DB\"\ndatabase_name = \"my-app-files\"\ndatabase_id = \"zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz\"\n```\n\nCreate databases via Wrangler:\n\n```bash\nwrangler d1 create my-app-db\nwrangler d1 create my-app-auth\nwrangler d1 create my-app-files\n```\n\n## Accessing Data via API\n\nAll data access in Quickback goes through the generated API endpoints—never direct database queries. This ensures security rules (firewall, access, guards, masking) are always enforced.\n\n### CRUD Operations\n\n```bash\n# List posts (firewall automatically filters by ownership)\nGET /api/v1/posts\n\n# Get a single post\nGET /api/v1/posts/:id\n\n# Create a post (guards validate allowed fields)\nPOST /api/v1/posts\n{ \"title\": \"Hello World\", \"content\": \"...\" }\n\n# Update a post (guards validate updatable fields)\nPATCH /api/v1/posts/:id\n{ \"title\": \"Updated Title\" }\n\n# Delete a post\nDELETE /api/v1/posts/:id\n```\n\n### Filtering and Pagination\n\n```bash\n# Filter by field\nGET /api/v1/posts?status=published\n\n# Pagination\nGET /api/v1/posts?limit=10&offset=20\n\n# Sort\nGET /api/v1/posts?sort=createdAt&order=desc\n```\n\n### Why No Direct Database Access?\n\nDirect database queries bypass Quickback's security layers:\n- **Firewall** - Data isolation by user/org/team\n- **Access** - Role-based permissions\n- **Guards** - Field-level create/update restrictions\n- **Masking** - Sensitive data redaction\n\nAlways use the API endpoints. For custom business logic, use [Actions](/compiler/definitions/actions).\n\n## Local Development\n\nD1 works locally with Wrangler:\n\n```bash\n# Start local dev server with D1\nwrangler dev\n\n# D1 data persists in .wrangler/state/\n```\n\nLocal D1 uses SQLite files in `.wrangler/state/v3/d1/`, which you can inspect with any SQLite client.\n\n## Security Architecture\n\nD1 uses application-layer security that is equally secure to Supabase RLS when using Quickback-generated code. The key difference is where enforcement happens.\n\n### How Security Works\n\n| Component | Enforcement | Notes |\n|-----------|-------------|-------|\n| CRUD endpoints | ✅ Firewall auto-applied | All generated routes enforce security |\n| Actions | ✅ Firewall auto-applied | Both standalone and record-based |\n| Manual routes | ⚠️ Must apply firewall | Use `withFirewall` helper |\n\n### Why D1 is Secure\n\n1. **No external database access** - D1 can only be queried through your Worker. There's no connection string or external endpoint.\n2. **Generated code enforces rules** - All CRUD and Action endpoints automatically apply firewall, access, guards, and masking.\n3. **Single entry point** - Every request flows through your API where security is enforced.\n\nUnlike Supabase where PostgreSQL RLS provides database-level enforcement, D1's security comes from architecture: the database is inaccessible except through your Worker, and all generated routes apply the four security pillars.\n\n### Comparison with Supabase RLS\n\n| Scenario | Supabase | D1 |\n|----------|----------|-----|\n| CRUD endpoints | ✅ Secure (RLS + App) | ✅ Secure (App) |\n| Actions | ✅ Secure (RLS + App) | ✅ Secure (App) |\n| Manual routes | ✅ RLS still protects | ⚠️ Must apply firewall |\n| External DB access | ⚠️ Possible with credentials | ✅ Not possible |\n| Dashboard queries | Via Supabase Studio | ⚠️ Admin only (audit logged) |\n\n### Writing Manual Routes\n\nIf you write custom routes outside of Quickback compilation (e.g., custom reports, integrations), use the generated `withFirewall` helper to ensure security:\n\n```ts\n\napp.get('/reports/monthly', async (c) => {\n return withFirewall(c, async (ctx, firewall) => {\n const results = await db.select()\n .from(invoices)\n .where(firewall);\n return c.json(results);\n });\n});\n```\n\nThe `withFirewall` helper:\n- Validates authentication\n- Builds the correct WHERE conditions for the current user/org\n- Returns 401 if not authenticated\n\n### Best Practices\n\n1. **Use generated endpoints** - Prefer CRUD and Actions over manual routes\n2. **Always apply firewall** - When writing manual routes, always use `withFirewall`\n3. **Avoid raw SQL** - Raw SQL bypasses application security; use Drizzle ORM\n4. **Review custom code** - Manual routes should be code-reviewed for security\n\n## Limitations\n\nD1 is built on SQLite, which means:\n\n- **No stored procedures** - Business logic lives in your Worker\n- **Single-writer** - One write connection at a time (reads scale horizontally)\n- **Size limits** - 10GB per database (free tier: 500MB)\n- **No PostgreSQL extensions** - Use Supabase if you need PostGIS, etc.\n\nFor most applications, these limits are non-issues. D1's edge distribution and zero-config setup outweigh the constraints."
326
+ "content": "Cloudflare D1 is SQLite at the edge. Quickback uses D1 as the primary database for Cloudflare deployments, with a multi-database pattern for separation of concerns.\n\n## What is D1?\n\nD1 is Cloudflare's serverless SQL database built on SQLite:\n\n- **Edge-native** - Runs in Cloudflare's global network\n- **SQLite compatible** - Use familiar SQL syntax\n- **Zero configuration** - No connection strings or pooling\n- **Automatic replication** - Read replicas at every edge location\n\n## Multi-Database Pattern\n\nQuickback generates separate D1 databases for different concerns:\n\n| Database | Binding | Purpose |\n|----------|---------|---------|\n| `AUTH_DB` | `AUTH_DB` | Better Auth tables (user, session, account) |\n| `DB` | `DB` | Your application data |\n| `FILES_DB` | `FILES_DB` | File metadata for R2 uploads |\n| `WEBHOOKS_DB` | `WEBHOOKS_DB` | Webhook delivery tracking |\n\nThis separation provides:\n- **Independent scaling** - Auth traffic doesn't affect app queries\n- **Isolation** - Auth schema changes don't touch your data\n- **Clarity** - Clear ownership of each database\n\n## Drizzle ORM Integration\n\nQuickback uses [Drizzle ORM](https://orm.drizzle.team/) for type-safe database access:\n\n```ts\n// schema/tables.ts - Your schema definition\n\nexport const posts = sqliteTable(\"posts\", {\n id: text(\"id\").primaryKey(),\n title: text(\"title\").notNull(),\n content: text(\"content\"),\n authorId: text(\"author_id\").notNull(),\n createdAt: integer(\"created_at\", { mode: \"timestamp\" }),\n});\n```\n\nThe compiler generates Drizzle queries based on your security rules:\n\n```ts\n// Generated query with firewall applied\nconst result = await db\n .select()\n .from(posts)\n .where(eq(posts.authorId, userId)); // Firewall injects ownership\n```\n\n## Migrations\n\nQuickback generates migrations automatically at compile time based on your schema changes:\n\n```bash\n# Compile your project (generates migrations)\nquickback compile\n\n# Apply migrations locally\nwrangler d1 migrations apply DB --local\n\n# Apply to production D1\nwrangler d1 migrations apply DB --remote\n```\n\nMigration files are generated in `drizzle/migrations/` and version-controlled with your code. You never need to manually generate migrations—just define your schema and compile.\n\n## wrangler.toml Bindings\n\nConfigure D1 bindings in `wrangler.toml`:\n\n```toml\n[[d1_databases]]\nbinding = \"DB\"\ndatabase_name = \"my-app-db\"\ndatabase_id = \"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\"\n\n[[d1_databases]]\nbinding = \"AUTH_DB\"\ndatabase_name = \"my-app-auth\"\ndatabase_id = \"yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy\"\n\n[[d1_databases]]\nbinding = \"FILES_DB\"\ndatabase_name = \"my-app-files\"\ndatabase_id = \"zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz\"\n```\n\nCreate databases via Wrangler:\n\n```bash\nwrangler d1 create my-app-db\nwrangler d1 create my-app-auth\nwrangler d1 create my-app-files\n```\n\n## Accessing Data via API\n\nAll data access in Quickback goes through the generated API endpoints—never direct database queries. This ensures security rules (firewall, access, guards, masking) are always enforced.\n\n### CRUD Operations\n\n```bash\n# List posts (firewall automatically filters by ownership)\nGET /api/v1/posts\n\n# Get a single post\nGET /api/v1/posts/:id\n\n# Create a post (guards validate allowed fields)\nPOST /api/v1/posts\n{ \"title\": \"Hello World\", \"content\": \"...\" }\n\n# Update a post (guards validate updatable fields)\nPATCH /api/v1/posts/:id\n{ \"title\": \"Updated Title\" }\n\n# Delete a post\nDELETE /api/v1/posts/:id\n```\n\n### Filtering and Pagination\n\n```bash\n# Filter by field\nGET /api/v1/posts?status=published\n\n# Pagination\nGET /api/v1/posts?limit=10&offset=20\n\n# Sort\nGET /api/v1/posts?sort=createdAt&order=desc\n```\n\n### Why No Direct Database Access?\n\nDirect database queries bypass Quickback's security layers:\n- **Firewall** - Data isolation by user/org/team\n- **Access** - Role-based permissions\n- **Guards** - Field-level create/update restrictions\n- **Masking** - Sensitive data redaction\n\nAlways use the API endpoints. For custom business logic, use [Actions](/compiler/definitions/actions).\n\n## Local Development\n\nD1 works locally with Wrangler:\n\n```bash\n# Start local dev server with D1\nwrangler dev\n\n# D1 data persists in .wrangler/state/\n```\n\nLocal D1 uses SQLite files in `.wrangler/state/v3/d1/`, which you can inspect with any SQLite client.\n\n## Security Architecture\n\nFor all Quickback-generated code, D1's application-layer security provides equivalent protection to Supabase RLS. The key difference is where enforcement happens — and this matters if you write custom routes.\n\n### How Security Works\n\n| Component | Enforcement | Notes |\n|-----------|-------------|-------|\n| CRUD endpoints | ✅ Firewall auto-applied | All generated routes enforce security |\n| Actions | ✅ Firewall auto-applied | Both standalone and record-based |\n| Manual routes | ⚠️ Must apply firewall | Use `withFirewall` helper |\n\n### Why D1 is Secure\n\n1. **No external database access** - D1 can only be queried through your Worker. There's no connection string or external endpoint.\n2. **Generated code enforces rules** - All CRUD and Action endpoints automatically apply firewall, access, guards, and masking.\n3. **Single entry point** - Every request flows through your API where security is enforced.\n\nUnlike Supabase where PostgreSQL RLS provides database-level enforcement (protecting data even if application code has bugs), D1's security comes from architecture: the database is inaccessible except through your Worker, and all generated routes apply the four security pillars. The trade-off is that **custom routes you write outside Quickback must manually apply security** — there is no database-level safety net.\n\n### Comparison with Supabase RLS\n\n| Scenario | Supabase | D1 |\n|----------|----------|-----|\n| CRUD endpoints | ✅ Secure (RLS + App) | ✅ Secure (App) |\n| Actions | ✅ Secure (RLS + App) | ✅ Secure (App) |\n| Manual routes | ✅ RLS still protects | ⚠️ Must apply firewall |\n| External DB access | ⚠️ Possible with credentials | ✅ Not possible |\n| Dashboard queries | Via Supabase Studio | ⚠️ Admin only (audit logged) |\n\n### Writing Manual Routes\n\nIf you write custom routes outside of Quickback compilation (e.g., custom reports, integrations), use the generated `withFirewall` helper to ensure security:\n\n```ts\n\napp.get('/reports/monthly', async (c) => {\n return withFirewall(c, async (ctx, firewall) => {\n const results = await db.select()\n .from(invoices)\n .where(firewall);\n return c.json(results);\n });\n});\n```\n\nThe `withFirewall` helper:\n- Validates authentication\n- Builds the correct WHERE conditions for the current user/org\n- Returns 401 if not authenticated\n\n### Best Practices\n\n1. **Use generated endpoints** - Prefer CRUD and Actions over manual routes\n2. **Always apply firewall** - When writing manual routes, always use `withFirewall`\n3. **Avoid raw SQL** - Raw SQL bypasses application security; use Drizzle ORM\n4. **Review custom code** - Manual routes should be code-reviewed for security\n\n## Limitations\n\nD1 is built on SQLite, which means:\n\n- **No stored procedures** - Business logic lives in your Worker\n- **Single-writer** - One write connection at a time (reads scale horizontally)\n- **Size limits** - 10GB per database (free tier: 500MB)\n- **No PostgreSQL extensions** - Use Supabase if you need PostGIS, etc.\n\nFor most applications, these limits are non-issues. D1's edge distribution and zero-config setup outweigh the constraints."
315
327
  },
316
328
  "stack/database": {
317
329
  "title": "Database",
@@ -319,7 +331,7 @@ export const DOCS = {
319
331
  },
320
332
  "stack/database/neon": {
321
333
  "title": "Neon",
322
- "content": "Neon provides serverless PostgreSQL that works with Cloudflare Workers via HTTP connections.\n\n## Configuration\n\n```typescript\ndatabase: defineDatabase(\"neon\", {\n connectionMode: 'auto',\n pooled: true,\n})\n```\n\n## Connection Modes\n\n- **HTTP Mode** For Cloudflare Workers and edge functions (stateless)\n- **WebSocket Mode** For Node.js and Bun (persistent connections)\n\nSee [Neon Integration](/compiler/integrations/neon) for the complete setup guide and RLS policy details."
334
+ "content": "Neon provides serverless PostgreSQL that works with Cloudflare Workers via HTTP connections. Choose Neon when you need PostgreSQL features (JSON operators, full-text search, advanced indexing) or database-level Row Level Security.\n\n## Configuration\n\n```typescript\n\nexport default defineConfig({\n name: \"my-app\",\n providers: {\n runtime: defineRuntime(\"cloudflare\"),\n database: defineDatabase(\"neon\", {\n connectionMode: \"auto\",\n pooled: true,\n }),\n auth: defineAuth(\"better-auth\"),\n },\n});\n```\n\n## Connection Modes\n\n| Mode | Best For | How It Works |\n|------|----------|-------------|\n| **HTTP** | Cloudflare Workers, edge functions | Stateless HTTP queries via `@neondatabase/serverless` |\n| **WebSocket** | Node.js, Bun | Persistent WebSocket connection for lower latency |\n| **Auto** (default) | Mixed environments | Detects runtime and picks the best mode |\n\n## Environment Variables\n\nSet your Neon connection string as a secret:\n\n```bash\n# For Cloudflare Workers\nnpx wrangler secret put DATABASE_URL\n# Paste your Neon connection string: postgresql://user:pass@ep-xxx.region.neon.tech/dbname\n\n# For local development (.env)\nDATABASE_URL=postgresql://user:pass@ep-xxx.region.neon.tech/dbname\n```\n\n## Migrations\n\nNeon uses PostgreSQL migrations (different from D1's SQLite migrations):\n\n```bash\n# Generate migrations after compiling\nquickback compile\n\n# Apply migrations\nnpm run db:migrate\n```\n\n## When to Choose Neon vs D1\n\n| Factor | Neon | D1 |\n|--------|------|-----|\n| SQL dialect | PostgreSQL | SQLite |\n| Security model | RLS + application-layer | Application-layer only |\n| Multi-region | Built-in replication | Single region per database |\n| Pricing | Free tier, then usage-based | Free tier, then usage-based |\n| Best for | Complex queries, existing Postgres apps | Edge-first, simple schemas |\n\n## See Also\n\n- [Neon Integration](/compiler/integrations/neon) Complete setup guide with RLS policy details\n- [D1](/stack/database/d1) — Alternative: SQLite at the edge\n- [Providers](/compiler/config/providers) — All database provider options"
323
335
  },
324
336
  "stack/database/using-d1": {
325
337
  "title": "Using D1",
@@ -327,11 +339,11 @@ export const DOCS = {
327
339
  },
328
340
  "stack": {
329
341
  "title": "Quickback Stack",
330
- "content": "Quickback Stack is the production-ready Cloudflare + Better Auth integration that runs entirely on YOUR Cloudflare account.\n\n## What is Quickback Stack?\n\nWhile the [Quickback Compiler](/compiler) transforms your definitions into deployable code, Quickback Stack is the runtime environment where that code runs. It's a complete backend architecture built on Cloudflare's edge platform:\n\n- **Your account, your data** — Everything runs on your Cloudflare account\n- **Edge-first** — Global distribution with sub-50ms latency worldwide\n- **Integrated services** — D1, R2, KV, Workers, and Durable Objects working together\n- **Production-ready auth** — Better Auth with plugins for every use case\n\n## Architecture Overview\n\n```\n┌─────────────────────────────────────────────────────────────┐\n│ Cloudflare Edge │\n├─────────────────────────────────────────────────────────────┤\n│ │\n│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │\n│ │ Workers │ │ Durable │ │ KV │ │\n│ │ (API) │ │ Objects │ │ (Cache) │ │\n│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │\n│ │ │ │ │\n│ ┌──────┴──────┐ ┌──────┴──────┐ ┌──────┴──────┐ │\n│ │ D1 │ │ Realtime │ │ Sessions │ │\n│ │ (SQLite) │ │ (WebSocket)│ │ (Auth) │ │\n│ └─────────────┘ └─────────────┘ └─────────────┘ │\n│ │\n│ ┌─────────────┐ ┌─────────────────────────────────┐ │\n│ │ R2 │ │ Better Auth │ │\n│ │ (Storage) │ │ (Email OTP, Passkeys, etc.) │ │\n│ └─────────────┘ └─────────────────────────────────┘ │\n│ │\n└─────────────────────────────────────────────────────────────┘\n```\n\n## Stack Components\n\n| Component | Service | Purpose |\n|-----------|---------|---------|\n| [Auth](/stack/auth) | Better Auth | Authentication, sessions, organizations |\n| [D1 Database](/stack/database/d1) | Cloudflare D1 | SQLite at the edge for your data |\n| [File Storage](/stack/storage/r2) | Cloudflare R2 | S3-compatible object storage |\n| [KV Storage](/stack/storage/kv) | Workers KV | Key-value for sessions and cache |\n| [Realtime](/stack/realtime/durable-objects) | Durable Objects | WebSocket connections for live updates |\n| [Embeddings](/stack/vector/embeddings) | Workers AI | Auto-generated vector embeddings |\n| [Queues](/stack/queues/handlers) | Cloudflare Queues | Background job processing |\n\n## What You Own\n\nEverything in Quickback Stack runs on your Cloudflare account:\n\n- **Databases** — Your D1 instances, your data\n- **Storage** — Your R2 buckets, your files\n- **Workers** — Your deployments, your logs\n- **Secrets** — Your API keys, stored in your Wrangler secrets\n\nQuickback provides the architecture and code generation. You own the infrastructure.\n\n## Quick Start\n\n1. **Cloudflare Account** — Sign up at [cloudflare.com](https://cloudflare.com)\n2. **Wrangler CLI** — `npm install -g wrangler`\n3. **Quickback CLI** — `npm install -g @kardoe/quickback`\n4. **Create Project** — `quickback create cloudflare my-app`\n5. **Compile** — `quickback compile`\n6. **Deploy** — `wrangler deploy`"
342
+ "content": "Quickback Stack is the production-ready Cloudflare + Better Auth integration that runs entirely on YOUR Cloudflare account.\n\n## What is Quickback Stack?\n\nWhile the [Quickback Compiler](/compiler) transforms your definitions into deployable code, Quickback Stack is the runtime environment where that code runs. It's a complete backend architecture built on Cloudflare's edge platform:\n\n- **Your account, your data** — Everything runs on your Cloudflare account\n- **Edge-first** — Global distribution with sub-50ms latency worldwide\n- **Integrated services** — D1, R2, KV, Workers, and Durable Objects working together\n- **Production-ready auth** — Better Auth with plugins for every use case\n\n## Architecture Overview\n\n```\n┌─────────────────────────────────────────────────────────────┐\n│ Cloudflare Edge │\n├─────────────────────────────────────────────────────────────┤\n│ │\n│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │\n│ │ Workers │ │ Durable │ │ KV │ │\n│ │ (API) │ │ Objects │ │ (Cache) │ │\n│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │\n│ │ │ │ │\n│ ┌──────┴──────┐ ┌──────┴──────┐ ┌──────┴──────┐ │\n│ │ D1 │ │ Realtime │ │ Sessions │ │\n│ │ (SQLite) │ │ (WebSocket)│ │ (Auth) │ │\n│ └─────────────┘ └─────────────┘ └─────────────┘ │\n│ │\n│ ┌─────────────┐ ┌─────────────────────────────────┐ │\n│ │ R2 │ │ Better Auth │ │\n│ │ (Storage) │ │ (Email OTP, Passkeys, etc.) │ │\n│ └─────────────┘ └─────────────────────────────────┘ │\n│ │\n└─────────────────────────────────────────────────────────────┘\n```\n\n## Stack Components\n\n| Component | Service | Purpose |\n|-----------|---------|---------|\n| [Auth](/stack/auth) | Better Auth | Authentication, sessions, organizations |\n| [D1 Database](/stack/database/d1) | Cloudflare D1 | SQLite at the edge for your data |\n| [File Storage](/stack/storage/r2) | Cloudflare R2 | S3-compatible object storage |\n| [KV Storage](/stack/storage/kv) | Workers KV | Key-value for sessions and cache |\n| [Realtime](/stack/realtime/durable-objects) | Durable Objects | WebSocket connections for live updates |\n| [Embeddings](/stack/vector/embeddings) | Workers AI | Auto-generated vector embeddings |\n| [Queues](/stack/queues/handlers) | Cloudflare Queues | Background job processing |\n\n## What You Own\n\nEverything in Quickback Stack runs on your Cloudflare account:\n\n- **Databases** — Your D1 instances, your data\n- **Storage** — Your R2 buckets, your files\n- **Workers** — Your deployments, your logs\n- **Secrets** — Your API keys, stored in your Wrangler secrets\n\nQuickback provides the architecture and code generation. You own the infrastructure.\n\n## Quick Start\n\n1. **Cloudflare Account** — Sign up at [cloudflare.com](https://cloudflare.com)\n2. **Wrangler CLI** — `npm install -g wrangler`\n3. **Quickback CLI** — `npm install -g @kardoe/quickback`\n4. **Create Project** — `quickback create cloudflare my-app`\n5. **Compile** — `quickback compile`\n6. **Deploy** — `wrangler deploy`\n\n## See Also\n\n- [Quickback Compiler](/compiler) — Define your schema and security rules that the Stack runs\n- [Definitions](/compiler/definitions) — Firewall, Access, Guards, Masking configuration\n- [Account UI](/account-ui) — Pre-built auth UI that works with your Stack deployment"
331
343
  },
332
344
  "stack/queues/handlers": {
333
345
  "title": "Custom Queue Handlers",
334
- "content": "Quickback lets you define custom queue handlers that integrate seamlessly with the generated queue consumer. This is useful for background processing like material extraction pipelines, batch jobs, and async workflows.\n\n## Overview\n\nCustom queue handlers are defined in the `services/queues/` directory using the `defineQueue` helper. The compiler extracts your handler logic and generates a unified queue consumer that dispatches messages based on their type.\n\n```\nquickback/\n├── definitions/\n│ ├── features/ # Data layer (CRUD, actions, security)\n│ ├── materials/\n│ └── claims/\n└── services/ # Infrastructure layer\n└── queues/\n├── ingest.ts # Material processing\n└── claim-batches.ts # Batch processing\n```\n\n## Defining a Queue Handler\n\nUse `defineQueue` to create a handler:\n\n```typescript\n// services/queues/ingest.ts\n\ninterface ProcessMaterialMessage {\n type: 'process_material';\n materialId: string;\n organizationId: string;\n}\n\nexport default defineQueue<ProcessMaterialMessage>({\n name: 'ingest',\n messageType: 'process_material',\n description: 'Process materials through extraction pipeline',\n\n execute: async ({ message, db, env, services, ack, retry }) => {\n const { materialId, organizationId } = message;\n\n // Dynamic import of schema\n const { materials } = await import('./features/materials/schema');\n\n // Get material from database\n const [material] = await db\n .select()\n .from(materials)\n .where(eq(materials.id, materialId))\n .limit(1);\n\n if (!material) {\n console.error('[IngestQueue] Material not found:', materialId);\n return { success: false, error: 'Material not found' };\n }\n\n // Process the material...\n await db\n .update(materials)\n .set({ extractionStatus: 'processing' })\n .where(eq(materials.id, materialId));\n\n // Your processing logic here\n\n return { success: true };\n },\n});\n```\n\n## Configuration Options\n\n| Option | Type | Required | Description |\n|--------|------|----------|-------------|\n| `name` | `string` | Yes | Handler identifier (used in logs) |\n| `messageType` | `string` | Yes | Message type to match (e.g., `'process_material'`) |\n| `description` | `string` | No | Human-readable description |\n| `execute` | `function` | Yes | Handler function |\n\n## Execute Function\n\nThe `execute` function receives a context object:\n\n```typescript\nexecute: async ({ message, db, env, services, ack, retry }) => {\n // message - The message payload (typed via generic)\n // db - Drizzle database instance\n // env - Cloudflare bindings (AI, queues, etc.)\n // services - Generated services (ai, etc.)\n // ack() - Acknowledge message (auto-called on success)\n // retry() - Retry message (auto-called on failure)\n\n return { success: true }; // or { success: false, error: 'reason' }\n}\n```\n\n## Constants and Imports\n\nThe compiler extracts top-level constants and imports from your handler file and includes them in the generated queue consumer:\n\n```typescript\n// services/queues/ingest.ts\n\nconst TEXT_GENERATION_MODEL = '@cf/meta/llama-3.1-8b-instruct';\n\nconst EXTRACTION_PROMPT = `You are a news analyst. Extract factual claims from this article.\n\nFor each claim, output a JSON object with:\n- content: The claim as a complete, standalone sentence\n- claimType: One of NEWS, QUOTE, OFFICIAL, ALLEGATION, STATISTIC, FINDING\n- urgency: 1-5 (5 = most urgent/breaking)\n\nOutput ONLY a JSON array of claims, no other text.`;\n\nexport default defineQueue({\n // ... handler definition\n});\n```\n\nThe compiler:\n1. Extracts all `import` statements\n2. Extracts top-level `const` declarations (including multi-line template literals)\n3. Inlines them in the generated queue consumer\n\n## Dynamic Schema Imports\n\nSince your schema files are generated by Quickback, use dynamic imports to reference them:\n\n```typescript\nexecute: async ({ message, db }) => {\n // Dynamic import - path is relative to generated src/ directory\n const { materials, claimExtractionBatches } = await import('./features/materials/schema');\n\n const [material] = await db\n .select()\n .from(materials)\n .where(eq(materials.id, message.materialId));\n\n // ...\n}\n```\n\n## Sending to Other Queues\n\nChain queue handlers by sending messages to other queues:\n\n```typescript\nexecute: async ({ message, db, env }) => {\n const { materialId } = message;\n\n // Process and create batch...\n const batchId = generateId('batch');\n\n // Send to next stage\n if (env.CLAIM_BATCHES_QUEUE) {\n await env.CLAIM_BATCHES_QUEUE.send({\n type: 'claim_batch_stage',\n batch_id: batchId,\n material_id: materialId,\n stage: 'subeditor',\n });\n }\n\n return { success: true };\n}\n```\n\n## Generated Output\n\nWhen queue handlers are defined, the compiler generates a unified `queue-consumer.ts`:\n\n```typescript\n// src/queue-consumer.ts (generated)\n\nconst TEXT_GENERATION_MODEL = '@cf/meta/llama-3.1-8b-instruct';\nconst EXTRACTION_PROMPT = `...`;\n\n// Embedding handler (if embeddings configured)\nconst embeddingQueueHandler = async (batch, env) => { ... };\n\n// Custom handler: ingest\nconst ingestQueueHandler = async (batch, env) => {\n const db = drizzle(env.DB);\n const services = createServices(env);\n\n for (const message of batch.messages) {\n try {\n const result = await executeHandler({\n message: message.body,\n env, db, services,\n ack: () => message.ack(),\n retry: () => message.retry(),\n });\n\n if (result.success) message.ack();\n else message.retry();\n } catch (error) {\n message.retry();\n }\n }\n};\n\n// Unified dispatcher\nexport const queue = async (batch, env) => {\n const messageType = batch.messages[0]?.body?.type;\n\n switch (messageType) {\n case 'embedding':\n await embeddingQueueHandler(batch, env);\n break;\n case 'process_material':\n await ingestQueueHandler(batch, env);\n break;\n default:\n console.warn('[Queue] Unknown message type:', messageType);\n for (const msg of batch.messages) msg.ack();\n }\n};\n```\n\n## wrangler.toml Configuration\n\nConfigure your queues in wrangler.toml:\n\n```toml\n# Queue producers (send to queues)\n[[queues.producers]]\nqueue = \"my-app-ingest-queue\"\nbinding = \"INGEST_QUEUE\"\n\n[[queues.producers]]\nqueue = \"my-app-claim-batches-queue\"\nbinding = \"CLAIM_BATCHES_QUEUE\"\n\n# Queue consumers (receive from queues)\n[[queues.consumers]]\nqueue = \"my-app-ingest-queue\"\nmax_batch_size = 10\nmax_batch_timeout = 30\nmax_retries = 3\n\n[[queues.consumers]]\nqueue = \"my-app-claim-batches-queue\"\nmax_batch_size = 10\nmax_batch_timeout = 30\nmax_retries = 3\n```\n\n## Deployment\n\nAfter defining queue handlers:\n\n1. **Create the queues:**\n ```bash\n wrangler queues create my-app-ingest-queue\n wrangler queues create my-app-claim-batches-queue\n ```\n\n2. **Compile:**\n ```bash\n quickback compile\n ```\n\n3. **Deploy:**\n ```bash\n wrangler deploy\n ```\n\n## Error Handling\n\nThe queue consumer handles errors automatically:\n\n- **Return `{ success: true }`** - Message is acknowledged\n- **Return `{ success: false, error: '...' }`** - Message is retried\n- **Throw an exception** - Message is retried\n\nAfter `max_retries` (default 3), failed messages are dropped or sent to a dead-letter queue if configured.\n\n```typescript\nexecute: async ({ message, db }) => {\n try {\n // Your processing logic\n return { success: true };\n } catch (error) {\n console.error('[Handler] Failed:', error);\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Unknown error'\n };\n }\n}\n```\n\n## Combining with Embeddings\n\nCustom queue handlers work alongside automatic embeddings. The compiler generates a single queue consumer that handles both:\n\n```typescript\nswitch (messageType) {\n case 'embedding':\n // Auto-generated embedding handler\n await embeddingQueueHandler(batch, env);\n break;\n case 'process_material':\n // Your custom handler\n await ingestQueueHandler(batch, env);\n break;\n}\n```\n\nSee [Automatic Embeddings](/stack/vector/embeddings) for configuring auto-embedding on CRUD operations."
346
+ "content": "Quickback lets you define custom queue handlers that integrate seamlessly with the generated queue consumer. This is useful for background processing like material extraction pipelines, batch jobs, and async workflows.\n\n## Overview\n\nCustom queue handlers are defined in the `services/queues/` directory using the `defineQueue` helper. The compiler extracts your handler logic and generates a unified queue consumer that dispatches messages based on their type.\n\n```\nquickback/\n├── features/ # Data layer (CRUD, actions, security)\n│ ├── materials/\n│ └── claims/\n└── services/ # Infrastructure layer\n └── queues/\n ├── ingest.ts # Material processing\n └── claim-batches.ts # Batch processing\n```\n\n## Defining a Queue Handler\n\nUse `defineQueue` to create a handler:\n\n```typescript\n// services/queues/ingest.ts\n\ninterface ProcessMaterialMessage {\n type: 'process_material';\n materialId: string;\n organizationId: string;\n}\n\nexport default defineQueue<ProcessMaterialMessage>({\n name: 'ingest',\n messageType: 'process_material',\n description: 'Process materials through extraction pipeline',\n\n execute: async ({ message, db, env, services, ack, retry }) => {\n const { materialId, organizationId } = message;\n\n // Dynamic import of schema\n const { materials } = await import('./features/materials/schema');\n\n // Get material from database\n const [material] = await db\n .select()\n .from(materials)\n .where(eq(materials.id, materialId))\n .limit(1);\n\n if (!material) {\n console.error('[IngestQueue] Material not found:', materialId);\n return { success: false, error: 'Material not found' };\n }\n\n // Process the material...\n await db\n .update(materials)\n .set({ extractionStatus: 'processing' })\n .where(eq(materials.id, materialId));\n\n // Your processing logic here\n\n return { success: true };\n },\n});\n```\n\n## Configuration Options\n\n| Option | Type | Required | Description |\n|--------|------|----------|-------------|\n| `name` | `string` | Yes | Handler identifier (used in logs) |\n| `messageType` | `string` | Yes | Message type to match (e.g., `'process_material'`) |\n| `description` | `string` | No | Human-readable description |\n| `execute` | `function` | Yes | Handler function |\n\n## Execute Function\n\nThe `execute` function receives a context object:\n\n```typescript\nexecute: async ({ message, db, env, services, ack, retry }) => {\n // message - The message payload (typed via generic)\n // db - Drizzle database instance\n // env - Cloudflare bindings (AI, queues, etc.)\n // services - Generated services (ai, etc.)\n // ack() - Acknowledge message (auto-called on success)\n // retry() - Retry message (auto-called on failure)\n\n return { success: true }; // or { success: false, error: 'reason' }\n}\n```\n\n## Constants and Imports\n\nThe compiler extracts top-level constants and imports from your handler file and includes them in the generated queue consumer:\n\n```typescript\n// services/queues/ingest.ts\n\nconst TEXT_GENERATION_MODEL = '@cf/meta/llama-3.1-8b-instruct';\n\nconst EXTRACTION_PROMPT = `You are a news analyst. Extract factual claims from this article.\n\nFor each claim, output a JSON object with:\n- content: The claim as a complete, standalone sentence\n- claimType: One of NEWS, QUOTE, OFFICIAL, ALLEGATION, STATISTIC, FINDING\n- urgency: 1-5 (5 = most urgent/breaking)\n\nOutput ONLY a JSON array of claims, no other text.`;\n\nexport default defineQueue({\n // ... handler definition\n});\n```\n\nThe compiler:\n1. Extracts all `import` statements\n2. Extracts top-level `const` declarations (including multi-line template literals)\n3. Inlines them in the generated queue consumer\n\n## Dynamic Schema Imports\n\nSince your schema files are generated by Quickback, use dynamic imports to reference them:\n\n```typescript\nexecute: async ({ message, db }) => {\n // Dynamic import - path is relative to generated src/ directory\n const { materials, claimExtractionBatches } = await import('./features/materials/schema');\n\n const [material] = await db\n .select()\n .from(materials)\n .where(eq(materials.id, message.materialId));\n\n // ...\n}\n```\n\n## Sending to Other Queues\n\nChain queue handlers by sending messages to other queues:\n\n```typescript\nexecute: async ({ message, db, env }) => {\n const { materialId } = message;\n\n // Process and create batch...\n const batchId = generateId('batch');\n\n // Send to next stage\n if (env.CLAIM_BATCHES_QUEUE) {\n await env.CLAIM_BATCHES_QUEUE.send({\n type: 'claim_batch_stage',\n batch_id: batchId,\n material_id: materialId,\n stage: 'subeditor',\n });\n }\n\n return { success: true };\n}\n```\n\n## Generated Output\n\nWhen queue handlers are defined, the compiler generates a unified `queue-consumer.ts`:\n\n```typescript\n// src/queue-consumer.ts (generated)\n\nconst TEXT_GENERATION_MODEL = '@cf/meta/llama-3.1-8b-instruct';\nconst EXTRACTION_PROMPT = `...`;\n\n// Embedding handler (if embeddings configured)\nconst embeddingQueueHandler = async (batch, env) => { ... };\n\n// Custom handler: ingest\nconst ingestQueueHandler = async (batch, env) => {\n const db = drizzle(env.DB);\n const services = createServices(env);\n\n for (const message of batch.messages) {\n try {\n const result = await executeHandler({\n message: message.body,\n env, db, services,\n ack: () => message.ack(),\n retry: () => message.retry(),\n });\n\n if (result.success) message.ack();\n else message.retry();\n } catch (error) {\n message.retry();\n }\n }\n};\n\n// Unified dispatcher\nexport const queue = async (batch, env) => {\n const messageType = batch.messages[0]?.body?.type;\n\n switch (messageType) {\n case 'embedding':\n await embeddingQueueHandler(batch, env);\n break;\n case 'process_material':\n await ingestQueueHandler(batch, env);\n break;\n default:\n console.warn('[Queue] Unknown message type:', messageType);\n for (const msg of batch.messages) msg.ack();\n }\n};\n```\n\n## wrangler.toml Configuration\n\nConfigure your queues in wrangler.toml:\n\n```toml\n# Queue producers (send to queues)\n[[queues.producers]]\nqueue = \"my-app-ingest-queue\"\nbinding = \"INGEST_QUEUE\"\n\n[[queues.producers]]\nqueue = \"my-app-claim-batches-queue\"\nbinding = \"CLAIM_BATCHES_QUEUE\"\n\n# Queue consumers (receive from queues)\n[[queues.consumers]]\nqueue = \"my-app-ingest-queue\"\nmax_batch_size = 10\nmax_batch_timeout = 30\nmax_retries = 3\n\n[[queues.consumers]]\nqueue = \"my-app-claim-batches-queue\"\nmax_batch_size = 10\nmax_batch_timeout = 30\nmax_retries = 3\n```\n\n## Deployment\n\nAfter defining queue handlers:\n\n1. **Create the queues:**\n ```bash\n wrangler queues create my-app-ingest-queue\n wrangler queues create my-app-claim-batches-queue\n ```\n\n2. **Compile:**\n ```bash\n quickback compile\n ```\n\n3. **Deploy:**\n ```bash\n wrangler deploy\n ```\n\n## Error Handling\n\nThe queue consumer handles errors automatically:\n\n- **Return `{ success: true }`** - Message is acknowledged\n- **Return `{ success: false, error: '...' }`** - Message is retried\n- **Throw an exception** - Message is retried\n\nAfter `max_retries` (default 3), failed messages are dropped or sent to a dead-letter queue if configured.\n\n```typescript\nexecute: async ({ message, db }) => {\n try {\n // Your processing logic\n return { success: true };\n } catch (error) {\n console.error('[Handler] Failed:', error);\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Unknown error'\n };\n }\n}\n```\n\n## Combining with Embeddings\n\nCustom queue handlers work alongside automatic embeddings. The compiler generates a single queue consumer that handles both:\n\n```typescript\nswitch (messageType) {\n case 'embedding':\n // Auto-generated embedding handler\n await embeddingQueueHandler(batch, env);\n break;\n case 'process_material':\n // Your custom handler\n await ingestQueueHandler(batch, env);\n break;\n}\n```\n\nSee [Automatic Embeddings](/stack/vector/embeddings) for configuring auto-embedding on CRUD operations."
335
347
  },
336
348
  "stack/queues": {
337
349
  "title": "Queues",
@@ -343,15 +355,15 @@ export const DOCS = {
343
355
  },
344
356
  "stack/realtime/durable-objects": {
345
357
  "title": "Realtime",
346
- "content": "Quickback can generate realtime notification helpers for broadcasting changes to connected clients. This enables live updates via WebSocket using Cloudflare Durable Objects.\n\n## Enabling Realtime\n\nAdd `realtime` configuration to individual table definitions:\n\n```typescript\n// features/claims/claims.ts\n\nexport const claims = sqliteTable(\"claims\", {\n id: text(\"id\").primaryKey(),\n title: text(\"title\").notNull(),\n status: text(\"status\").notNull(),\n organizationId: text(\"organization_id\").notNull(),\n});\n\nexport default defineTable(claims, {\n firewall: { organization: {} },\n realtime: {\n enabled: true, // Enable realtime for this table\n onInsert: true, // Broadcast on INSERT (default: true)\n onUpdate: true, // Broadcast on UPDATE (default: true)\n onDelete: true, // Broadcast on DELETE (default: true)\n requiredRoles: [\"member\", \"admin\"], // Who receives broadcasts\n fields: [\"id\", \"title\", \"status\"], // Fields to include (optional)\n },\n // ... guards, crud\n});\n```\n\nWhen any table has realtime enabled, the compiler generates `src/lib/realtime.ts` with helper functions for sending notifications.\n\n## Generated Helper\n\nThe `createRealtime()` factory provides convenient methods for both Postgres Changes (CRUD events) and custom broadcasts:\n\n```typescript\n\nexport const execute: ActionExecutor = async ({ db, ctx, input }) => {\n const realtime = createRealtime(ctx.env);\n\n // After creating a record\n await realtime.insert('claims', newClaim, ctx.activeOrgId!);\n\n // After updating a record\n await realtime.update('claims', newClaim, oldClaim, ctx.activeOrgId!);\n\n // After deleting a record\n await realtime.delete('claims', { id: claimId }, ctx.activeOrgId!);\n\n return { success: true };\n};\n```\n\n## Event Format\n\nQuickback broadcasts events in a structured JSON format for easy client-side handling.\n\n### Insert Event\n\n```typescript\nawait realtime.insert('materials', {\n id: 'mat_123',\n title: 'Breaking News',\n status: 'pending'\n}, ctx.activeOrgId!);\n```\n\nBroadcasts:\n```json\n{\n \"type\": \"postgres_changes\",\n \"table\": \"materials\",\n \"eventType\": \"INSERT\",\n \"schema\": \"public\",\n \"new\": {\n \"id\": \"mat_123\",\n \"title\": \"Breaking News\",\n \"status\": \"pending\"\n },\n \"old\": null\n}\n```\n\n### Update Event\n\n```typescript\nawait realtime.update('materials',\n { id: 'mat_123', status: 'completed' }, // new\n { id: 'mat_123', status: 'pending' }, // old\n ctx.activeOrgId!\n);\n```\n\nBroadcasts:\n```json\n{\n \"type\": \"postgres_changes\",\n \"table\": \"materials\",\n \"eventType\": \"UPDATE\",\n \"schema\": \"public\",\n \"new\": { \"id\": \"mat_123\", \"status\": \"completed\" },\n \"old\": { \"id\": \"mat_123\", \"status\": \"pending\" }\n}\n```\n\n### Delete Event\n\n```typescript\nawait realtime.delete('materials',\n { id: 'mat_123' },\n ctx.activeOrgId!\n);\n```\n\nBroadcasts:\n```json\n{\n \"type\": \"postgres_changes\",\n \"table\": \"materials\",\n \"eventType\": \"DELETE\",\n \"schema\": \"public\",\n \"new\": null,\n \"old\": { \"id\": \"mat_123\" }\n}\n```\n\n## Custom Broadcasts\n\nFor arbitrary events that don't map to CRUD operations:\n\n```typescript\nawait realtime.broadcast('processing-complete', {\n materialId: 'mat_123',\n claimCount: 5,\n duration: 1234\n}, ctx.activeOrgId!);\n```\n\nBroadcasts:\n```json\n{\n \"type\": \"broadcast\",\n \"event\": \"processing-complete\",\n \"payload\": {\n \"materialId\": \"mat_123\",\n \"claimCount\": 5,\n \"duration\": 1234\n }\n}\n```\n\n## User-Specific Broadcasts\n\nTarget a specific user instead of the entire organization:\n\n```typescript\n// Only this user receives the notification\nawait realtime.insert('notifications', newNotification, ctx.activeOrgId!, {\n userId: ctx.userId\n});\n\nawait realtime.broadcast('task-assigned', {\n taskId: 'task_123'\n}, ctx.activeOrgId!, {\n userId: assigneeId\n});\n```\n\n## Role-Based Filtering\n\nLimit which roles receive a broadcast using `targetRoles`:\n\n```typescript\n// Only admins and editors receive this broadcast\nawait realtime.insert('admin-actions', action, ctx.activeOrgId!, {\n targetRoles: ['admin', 'editor']\n});\n\n// Members won't see this update\nawait realtime.update('sensitive-data', newRecord, oldRecord, ctx.activeOrgId!, {\n targetRoles: ['admin']\n});\n```\n\nIf `targetRoles` is not specified, all authenticated users in the organization receive the broadcast.\n\n## Per-Role Field Masking\n\nApply different field masking based on the subscriber's role using `maskingConfig`:\n\n```typescript\nawait realtime.insert('employees', newEmployee, ctx.activeOrgId!, {\n maskingConfig: {\n ssn: { type: 'ssn', show: { roles: ['admin', 'hr'] } },\n salary: { type: 'redact', show: { roles: ['admin'] } },\n email: { type: 'email', show: { roles: ['admin', 'hr', 'manager'] } },\n },\n});\n```\n\n**How masking works:**\n- Each subscriber receives a payload masked according to their role\n- Roles in the `show.roles` array see unmasked values\n- All other roles see the masked version\n- Masking is pre-computed per-role (O(roles) not O(subscribers))\n\n**Available mask types:**\n| Type | Example Output |\n|------|----------------|\n| `email` | `j***@y*********.com` |\n| `phone` | `******4567` |\n| `ssn` | `*****6789` |\n| `creditCard` | `**** **** **** 1111` |\n| `name` | `J***` |\n| `redact` | `[REDACTED]` |\n\n### Owner-Based Masking\n\nShow unmasked data to the record owner:\n\n```typescript\nmaskingConfig: {\n ssn: {\n type: 'ssn',\n show: { roles: ['admin'], or: 'owner' } // Admin OR owner sees unmasked\n },\n}\n```\n\n## Client-Side Subscription\n\n### WebSocket Authentication\n\nThe Broadcaster supports two authentication methods:\n\n**Session Token (Browser/App):**\n```typescript\nconst ws = new WebSocket('wss://api.yourdomain.com/realtime/v1/websocket');\n\nws.onopen = () => {\n ws.send(JSON.stringify({\n type: 'auth',\n token: sessionToken, // JWT from Better Auth session\n organizationId: activeOrgId,\n }));\n};\n\nws.onmessage = (e) => {\n const msg = JSON.parse(e.data);\n if (msg.type === 'auth_success') {\n console.log('Authenticated:', {\n role: msg.role,\n roles: msg.roles,\n authMethod: msg.authMethod, // 'session'\n });\n }\n};\n```\n\n**API Key (Server/CLI):**\n```typescript\nconst ws = new WebSocket('wss://api.yourdomain.com/realtime/v1/websocket');\n\nws.onopen = () => {\n ws.send(JSON.stringify({\n type: 'auth',\n token: apiKey, // API key for machine-to-machine auth\n organizationId: orgId,\n }));\n};\n\n// authMethod will be 'api_key' on success\n```\n\n### Handling Messages\n\n```typescript\nws.onmessage = (event) => {\n const msg = JSON.parse(event.data);\n\n // Handle CRUD events\n if (msg.type === 'postgres_changes') {\n const { table, eventType, new: newRecord, old: oldRecord } = msg;\n\n if (eventType === 'INSERT') {\n addRecord(table, newRecord);\n } else if (eventType === 'UPDATE') {\n updateRecord(table, newRecord);\n } else if (eventType === 'DELETE') {\n removeRecord(table, oldRecord.id);\n }\n }\n\n // Handle custom broadcasts\n if (msg.type === 'broadcast') {\n const { event, payload } = msg;\n\n if (event === 'processing-complete') {\n refreshMaterial(payload.materialId);\n } else if (event === 'task-assigned') {\n showTaskNotification(payload.taskId);\n }\n }\n};\n```\n\n## Required Environment Variables\n\n| Variable | Description |\n|----------|-------------|\n| `REALTIME_URL` | URL of the broadcast/realtime worker |\n| `ACCESS_TOKEN` | Internal service-to-service auth token |\n\nExample configuration:\n\n```toml\n# wrangler.toml\n[vars]\nREALTIME_URL = \"https://your-realtime-worker.workers.dev\"\nACCESS_TOKEN = \"your-internal-secret-token\"\n```\n\n## Architecture\n\n```\n┌─────────────┐ POST /broadcast ┌──────────────────┐\n│ API Worker │ ───────────────────────► │ Realtime Worker │\n│ (Quickback)│ │ (Durable Object)│\n└─────────────┘ └────────┬─────────┘\n │\n WebSocket│\n │\n ┌────────▼─────────┐\n │ Browser Clients │\n │ (WebSocket) │\n └──────────────────┘\n```\n\n1. **API Worker** - Your Quickback-generated API. Calls `realtime.insert()` etc. after CRUD operations.\n2. **Realtime Worker** - Separate worker with Durable Object for managing WebSocket connections.\n3. **Browser Clients** - Connect via WebSocket, subscribe to channels.\n\n## Best Practices\n\n### Broadcast After Commit\n\nAlways broadcast after the database operation succeeds:\n\n```typescript\n// Good - broadcast after successful insert\nconst [newClaim] = await db.insert(claims).values(data).returning();\nawait realtime.insert('claims', newClaim, ctx.activeOrgId!);\n\n// Bad - don't broadcast before confirming success\nawait realtime.insert('claims', data, ctx.activeOrgId!);\nawait db.insert(claims).values(data); // Could fail!\n```\n\n### Minimal Payloads\n\nOnly include necessary data in broadcasts:\n\n```typescript\n// Good - minimal payload\nawait realtime.update('materials',\n { id: record.id, status: 'completed' },\n { id: record.id, status: 'pending' },\n ctx.activeOrgId!\n);\n\n// Avoid - sending entire record with large content\nawait realtime.update('materials', fullRecord, oldRecord, ctx.activeOrgId!);\n```\n\n### Use Broadcasts for Complex Events\n\nFor events that don't map cleanly to CRUD:\n\n```typescript\n// Processing pipeline completed\nawait realtime.broadcast('pipeline-complete', {\n materialId: material.id,\n stages: ['fetch', 'extract', 'analyze'],\n claimsCreated: 5,\n quotesExtracted: 3\n}, ctx.activeOrgId!);\n```\n\n## Custom Event Namespaces\n\nFor complex applications with many custom events, you can define typed event namespaces using `defineRealtime`. This generates strongly-typed helper methods for your custom events.\n\n### Defining Event Namespaces\n\nCreate a file in `services/realtime/`:\n\n```typescript\n// services/realtime/extraction.ts\n\nexport default defineRealtime({\n name: 'extraction',\n events: ['started', 'progress', 'completed', 'failed'],\n description: 'Material extraction pipeline events',\n});\n```\n\n### Generated Helper Methods\n\nAfter compilation, the `createRealtime()` helper includes your custom namespace:\n\n```typescript\n\nexport const execute: ActionExecutor = async ({ ctx, input }) => {\n const realtime = createRealtime(ctx.env);\n\n // Type-safe custom event methods\n await realtime.extraction.started({\n materialId: input.materialId,\n }, ctx.activeOrgId!);\n\n // Progress updates\n await realtime.extraction.progress({\n materialId: input.materialId,\n percent: 50,\n stage: 'extracting',\n }, ctx.activeOrgId!);\n\n // Completion\n await realtime.extraction.completed({\n materialId: input.materialId,\n claimsExtracted: 15,\n duration: 1234,\n }, ctx.activeOrgId!);\n\n return { success: true };\n};\n```\n\n### Event Format\n\nCustom namespace events use the `broadcast` type with namespaced event names:\n\n```json\n{\n \"type\": \"broadcast\",\n \"event\": \"extraction:started\",\n \"payload\": {\n \"materialId\": \"mat_123\"\n }\n}\n```\n\n### Multiple Namespaces\n\nDefine multiple namespaces for different parts of your application:\n\n```typescript\n// services/realtime/notifications.ts\nexport default defineRealtime({\n name: 'notifications',\n events: ['new', 'read', 'dismissed'],\n description: 'User notification events',\n});\n\n// services/realtime/presence.ts\nexport default defineRealtime({\n name: 'presence',\n events: ['joined', 'left', 'typing', 'idle'],\n description: 'User presence events',\n});\n```\n\nUsage:\n```typescript\nconst realtime = createRealtime(ctx.env);\n\n// Notification events\nawait realtime.notifications.new({ ... }, ctx.activeOrgId!);\n\n// Presence events\nawait realtime.presence.joined({ userId: ctx.userId }, ctx.activeOrgId!);\n```\n\n### Namespace vs Generic Broadcast\n\n| Use Case | Approach |\n|----------|----------|\n| One-off custom event | `realtime.broadcast('event-name', payload, orgId)` |\n| Repeated event patterns | `defineRealtime` namespace |\n| Type-safe events | `defineRealtime` namespace |\n| Event discovery | `defineRealtime` (appears in generated types) |"
358
+ "content": "Quickback can generate realtime notification helpers for broadcasting changes to connected clients. This enables live updates via WebSocket using Cloudflare Durable Objects.\n\n## Enabling Realtime\n\nAdd `realtime` configuration to individual table definitions:\n\n```typescript\n// features/applications/applications.ts\n\nexport const applications = sqliteTable(\"applications\", {\n id: text(\"id\").primaryKey(),\n candidateId: text(\"candidate_id\").notNull(),\n jobId: text(\"job_id\").notNull(),\n stage: text(\"stage\").notNull(),\n organizationId: text(\"organization_id\").notNull(),\n});\n\nexport default defineTable(applications, {\n firewall: { organization: {} },\n realtime: {\n enabled: true, // Enable realtime for this table\n onInsert: true, // Broadcast on INSERT (default: true)\n onUpdate: true, // Broadcast on UPDATE (default: true)\n onDelete: true, // Broadcast on DELETE (default: true)\n requiredRoles: [\"recruiter\", \"hiring-manager\"], // Who receives broadcasts\n fields: [\"id\", \"candidateId\", \"stage\"], // Fields to include (optional)\n },\n // ... guards, crud\n});\n```\n\nWhen any table has realtime enabled, the compiler generates `src/lib/realtime.ts` with helper functions for sending notifications.\n\n## Generated Helper\n\nThe `createRealtime()` factory provides convenient methods for both Postgres Changes (CRUD events) and custom broadcasts:\n\n```typescript\n\nexport const execute: ActionExecutor = async ({ db, ctx, input }) => {\n const realtime = createRealtime(ctx.env);\n\n // After creating a record\n await realtime.insert('applications', newApplication, ctx.activeOrgId!);\n\n // After updating a record\n await realtime.update('applications', newApplication, oldApplication, ctx.activeOrgId!);\n\n // After deleting a record\n await realtime.delete('applications', { id: applicationId }, ctx.activeOrgId!);\n\n return { success: true };\n};\n```\n\n## Event Format\n\nQuickback broadcasts events in a structured JSON format for easy client-side handling.\n\n### Insert Event\n\n```typescript\nawait realtime.insert('applications', {\n id: 'app_123',\n candidateId: 'cnd_456',\n stage: 'applied'\n}, ctx.activeOrgId!);\n```\n\nBroadcasts:\n```json\n{\n \"type\": \"postgres_changes\",\n \"table\": \"applications\",\n \"eventType\": \"INSERT\",\n \"schema\": \"public\",\n \"new\": {\n \"id\": \"app_123\",\n \"candidateId\": \"cnd_456\",\n \"stage\": \"applied\"\n },\n \"old\": null\n}\n```\n\n### Update Event\n\n```typescript\nawait realtime.update('applications',\n { id: 'app_123', stage: 'interview' }, // new\n { id: 'app_123', stage: 'screening' }, // old\n ctx.activeOrgId!\n);\n```\n\nBroadcasts:\n```json\n{\n \"type\": \"postgres_changes\",\n \"table\": \"applications\",\n \"eventType\": \"UPDATE\",\n \"schema\": \"public\",\n \"new\": { \"id\": \"app_123\", \"stage\": \"interview\" },\n \"old\": { \"id\": \"app_123\", \"stage\": \"screening\" }\n}\n```\n\n### Delete Event\n\n```typescript\nawait realtime.delete('applications',\n { id: 'app_123' },\n ctx.activeOrgId!\n);\n```\n\nBroadcasts:\n```json\n{\n \"type\": \"postgres_changes\",\n \"table\": \"applications\",\n \"eventType\": \"DELETE\",\n \"schema\": \"public\",\n \"new\": null,\n \"old\": { \"id\": \"app_123\" }\n}\n```\n\n## Custom Broadcasts\n\nFor arbitrary events that don't map to CRUD operations:\n\n```typescript\nawait realtime.broadcast('screening-complete', {\n applicationId: 'app_123',\n candidateId: 'cnd_456',\n stage: 'interview'\n}, ctx.activeOrgId!);\n```\n\nBroadcasts:\n```json\n{\n \"type\": \"broadcast\",\n \"event\": \"screening-complete\",\n \"payload\": {\n \"applicationId\": \"app_123\",\n \"candidateId\": \"cnd_456\",\n \"stage\": \"interview\"\n }\n}\n```\n\n## User-Specific Broadcasts\n\nTarget a specific user instead of the entire organization:\n\n```typescript\n// Only this user receives the notification\nawait realtime.insert('notifications', newNotification, ctx.activeOrgId!, {\n userId: ctx.userId\n});\n\n// Notify a hiring manager of a new application\nawait realtime.broadcast('application-received', {\n applicationId: 'app_123',\n jobId: 'job_789'\n}, ctx.activeOrgId!, {\n userId: hiringManagerId\n});\n```\n\n## Role-Based Filtering\n\nLimit which roles receive a broadcast using `targetRoles`:\n\n```typescript\n// Only hiring managers and recruiters receive this broadcast\nawait realtime.insert('applications', application, ctx.activeOrgId!, {\n targetRoles: ['hiring-manager', 'recruiter']\n});\n\n// Interviewers only see applications assigned to them\nawait realtime.update('applications', newRecord, oldRecord, ctx.activeOrgId!, {\n targetRoles: ['hiring-manager']\n});\n```\n\nIf `targetRoles` is not specified, all authenticated users in the organization receive the broadcast.\n\n## Per-Role Field Masking\n\nApply different field masking based on the subscriber's role using `maskingConfig`:\n\n```typescript\nawait realtime.insert('candidates', newCandidate, ctx.activeOrgId!, {\n maskingConfig: {\n phone: { type: 'phone', show: { roles: ['owner', 'hiring-manager', 'recruiter'] } },\n email: { type: 'email', show: { roles: ['owner', 'hiring-manager', 'recruiter'] } },\n resumeUrl: { type: 'redact', show: { roles: ['owner', 'hiring-manager', 'recruiter'] } },\n },\n});\n```\n\n**How masking works:**\n- Each subscriber receives a payload masked according to their role\n- Roles in the `show.roles` array see unmasked values\n- All other roles see the masked version\n- Masking is pre-computed per-role (O(roles) not O(subscribers))\n\n**Available mask types:**\n| Type | Example Output |\n|------|----------------|\n| `email` | `j***@y*********.com` |\n| `phone` | `******4567` |\n| `ssn` | `*****6789` |\n| `creditCard` | `**** **** **** 1111` |\n| `name` | `J***` |\n| `redact` | `[REDACTED]` |\n\n### Owner-Based Masking\n\nShow unmasked data to the record owner:\n\n```typescript\nmaskingConfig: {\n phone: {\n type: 'phone',\n show: { roles: ['hiring-manager'], or: 'owner' } // Hiring manager OR owner sees unmasked\n },\n}\n```\n\n## Client-Side Subscription\n\n### WebSocket Authentication\n\nThe Broadcaster supports two authentication methods:\n\n**Session Token (Browser/App):**\n```typescript\nconst ws = new WebSocket('wss://api.yourdomain.com/realtime/v1/websocket');\n\nws.onopen = () => {\n ws.send(JSON.stringify({\n type: 'auth',\n token: sessionToken, // JWT from Better Auth session\n organizationId: activeOrgId,\n }));\n};\n\nws.onmessage = (e) => {\n const msg = JSON.parse(e.data);\n if (msg.type === 'auth_success') {\n console.log('Authenticated:', {\n role: msg.role,\n roles: msg.roles,\n authMethod: msg.authMethod, // 'session'\n });\n }\n};\n```\n\n**API Key (Server/CLI):**\n```typescript\nconst ws = new WebSocket('wss://api.yourdomain.com/realtime/v1/websocket');\n\nws.onopen = () => {\n ws.send(JSON.stringify({\n type: 'auth',\n token: apiKey, // API key for machine-to-machine auth\n organizationId: orgId,\n }));\n};\n\n// authMethod will be 'api_key' on success\n```\n\n### Handling Messages\n\n```typescript\nws.onmessage = (event) => {\n const msg = JSON.parse(event.data);\n\n // Handle CRUD events\n if (msg.type === 'postgres_changes') {\n const { table, eventType, new: newRecord, old: oldRecord } = msg;\n\n if (eventType === 'INSERT') {\n addRecord(table, newRecord);\n } else if (eventType === 'UPDATE') {\n updateRecord(table, newRecord);\n } else if (eventType === 'DELETE') {\n removeRecord(table, oldRecord.id);\n }\n }\n\n // Handle custom broadcasts\n if (msg.type === 'broadcast') {\n const { event, payload } = msg;\n\n if (event === 'screening-complete') {\n refreshApplication(payload.applicationId);\n } else if (event === 'interview-scheduled') {\n showInterviewNotification(payload.applicationId);\n }\n }\n};\n```\n\n## Required Environment Variables\n\n| Variable | Description |\n|----------|-------------|\n| `REALTIME_URL` | URL of the broadcast/realtime worker |\n| `ACCESS_TOKEN` | Internal service-to-service auth token |\n\nExample configuration:\n\n```toml\n# wrangler.toml\n[vars]\nREALTIME_URL = \"https://your-realtime-worker.workers.dev\"\nACCESS_TOKEN = \"your-internal-secret-token\"\n```\n\n## Architecture\n\n```\n┌─────────────┐ POST /broadcast ┌──────────────────┐\n│ API Worker │ ───────────────────────► │ Realtime Worker │\n│ (Quickback)│ │ (Durable Object)│\n└─────────────┘ └────────┬─────────┘\n │\n WebSocket│\n │\n ┌────────▼─────────┐\n │ Browser Clients │\n │ (WebSocket) │\n └──────────────────┘\n```\n\n1. **API Worker** - Your Quickback-generated API. Calls `realtime.insert()` etc. after CRUD operations.\n2. **Realtime Worker** - Separate worker with Durable Object for managing WebSocket connections.\n3. **Browser Clients** - Connect via WebSocket, subscribe to channels.\n\n## Best Practices\n\n### Broadcast After Commit\n\nAlways broadcast after the database operation succeeds:\n\n```typescript\n// Good - broadcast after successful insert\nconst [newApp] = await db.insert(applications).values(data).returning();\nawait realtime.insert('applications', newApp, ctx.activeOrgId!);\n\n// Bad - don't broadcast before confirming success\nawait realtime.insert('applications', data, ctx.activeOrgId!);\nawait db.insert(applications).values(data); // Could fail!\n```\n\n### Minimal Payloads\n\nOnly include necessary data in broadcasts:\n\n```typescript\n// Good - minimal payload\nawait realtime.update('applications',\n { id: record.id, stage: 'interview' },\n { id: record.id, stage: 'screening' },\n ctx.activeOrgId!\n);\n\n// Avoid - sending entire record with large content\nawait realtime.update('applications', fullRecord, oldRecord, ctx.activeOrgId!);\n```\n\n### Use Broadcasts for Complex Events\n\nFor events that don't map cleanly to CRUD:\n\n```typescript\n// Hiring pipeline stage completed\nawait realtime.broadcast('pipeline-stage-complete', {\n applicationId: application.id,\n stages: ['applied', 'screening', 'interview'],\n currentStage: 'offer',\n candidateId: application.candidateId\n}, ctx.activeOrgId!);\n```\n\n## Custom Event Namespaces\n\nFor complex applications with many custom events, you can define typed event namespaces using `defineRealtime`. This generates strongly-typed helper methods for your custom events.\n\n### Defining Event Namespaces\n\nCreate a file in `services/realtime/`:\n\n```typescript\n// services/realtime/screening.ts\n\nexport default defineRealtime({\n name: 'screening',\n events: ['started', 'progress', 'completed', 'failed'],\n description: 'Candidate screening pipeline events',\n});\n```\n\n### Generated Helper Methods\n\nAfter compilation, the `createRealtime()` helper includes your custom namespace:\n\n```typescript\n\nexport const execute: ActionExecutor = async ({ ctx, input }) => {\n const realtime = createRealtime(ctx.env);\n\n // Type-safe custom event methods\n await realtime.screening.started({\n applicationId: input.applicationId,\n }, ctx.activeOrgId!);\n\n // Progress updates\n await realtime.screening.progress({\n applicationId: input.applicationId,\n percent: 50,\n step: 'background-check',\n }, ctx.activeOrgId!);\n\n // Completion\n await realtime.screening.completed({\n applicationId: input.applicationId,\n result: 'passed',\n duration: 1234,\n }, ctx.activeOrgId!);\n\n return { success: true };\n};\n```\n\n### Event Format\n\nCustom namespace events use the `broadcast` type with namespaced event names:\n\n```json\n{\n \"type\": \"broadcast\",\n \"event\": \"screening:started\",\n \"payload\": {\n \"applicationId\": \"app_123\"\n }\n}\n```\n\n### Multiple Namespaces\n\nDefine multiple namespaces for different parts of your application:\n\n```typescript\n// services/realtime/notifications.ts\nexport default defineRealtime({\n name: 'notifications',\n events: ['interview-reminder', 'offer-update', 'application-received'],\n description: 'Recruitment notification events',\n});\n\n// services/realtime/presence.ts\nexport default defineRealtime({\n name: 'presence',\n events: ['joined', 'left', 'reviewing', 'idle'],\n description: 'Who is reviewing which candidate',\n});\n```\n\nUsage:\n```typescript\nconst realtime = createRealtime(ctx.env);\n\n// Notification events\nawait realtime.notifications['interview-reminder']({ ... }, ctx.activeOrgId!);\n\n// Presence events — who's reviewing which candidate\nawait realtime.presence.reviewing({ userId: ctx.userId, candidateId: 'cnd_456' }, ctx.activeOrgId!);\n```\n\n### Namespace vs Generic Broadcast\n\n| Use Case | Approach |\n|----------|----------|\n| One-off custom event | `realtime.broadcast('event-name', payload, orgId)` |\n| Repeated event patterns | `defineRealtime` namespace |\n| Type-safe events | `defineRealtime` namespace |\n| Event discovery | `defineRealtime` (appears in generated types) |"
347
359
  },
348
360
  "stack/realtime": {
349
361
  "title": "Realtime",
350
- "content": "The Quickback Stack uses Cloudflare Durable Objects to broadcast real-time updates over WebSockets. CRUD events and custom broadcasts are delivered to connected clients with the same security layers (firewall isolation, role-based filtering, field masking) applied.\n\n## Architecture\n\n```\n┌─────────────┐ POST /broadcast ┌──────────────────┐\n│ API Worker │ ───────────────────────► │ Realtime Worker │\n│ (Quickback) │ │ (Durable Object) │\n└─────────────┘ └────────┬─────────┘\n │\n WebSocket│\n │\n ┌────────▼─────────┐\n │ Browser Clients │\n │ (WebSocket) │\n └──────────────────┘\n```\n\n1. **API Worker** — Your Quickback-compiled API. Calls `realtime.insert()` etc. after CRUD operations.\n2. **Realtime Worker** — Separate Cloudflare Worker with Durable Object for managing WebSocket connections, one per organization.\n3. **Browser Clients** — Connect via WebSocket, receive filtered and masked broadcasts.\n\n## Key Features\n\n| Feature | Description |\n|---------|-------------|\n| Organization-scoped | Each org gets its own Durable Object instance |\n| Role-based filtering | Only send events to users with matching roles |\n| Per-role masking | Different users see different field values based on their role |\n| User-specific targeting | Send events to a specific user within an org |\n| Custom broadcasts | Arbitrary events beyond CRUD |\n| Custom namespaces | `defineRealtime()` for type-safe event helpers |\n| Two auth methods | Session tokens (browser) and API keys (server/CLI) |\n\n## Enabling Realtime\n\nAdd `realtime` to individual table definitions:\n\n```typescript\nexport default defineTable(claims, {\n firewall: { organization: {} },\n realtime: {\n enabled: true,\n onInsert: true,\n onUpdate: true,\n onDelete: true,\n requiredRoles: [\"member\", \"admin\"],\n fields: [\"id\", \"title\", \"status\"],\n },\n});\n```\n\nAnd enable the realtime binding in your database config. The compiler generates the Durable Object worker and helper functions.\n\n## Pages\n\n- **[Durable Objects Setup](/stack/realtime/durable-objects)** — Configuration, wrangler bindings, event formats, masking, and custom namespaces\n- **[Using Realtime](/stack/realtime/using-realtime)** — WebSocket connection, authentication, and client-side handling"
362
+ "content": "The Quickback Stack uses Cloudflare Durable Objects to broadcast real-time updates over WebSockets. CRUD events and custom broadcasts are delivered to connected clients with the same security layers (firewall isolation, role-based filtering, field masking) applied.\n\n## Architecture\n\n```\n┌─────────────┐ POST /broadcast ┌──────────────────┐\n│ API Worker │ ───────────────────────► │ Realtime Worker │\n│ (Quickback) │ │ (Durable Object) │\n└─────────────┘ └────────┬─────────┘\n │\n WebSocket│\n │\n ┌────────▼─────────┐\n │ Browser Clients │\n │ (WebSocket) │\n └──────────────────┘\n```\n\n1. **API Worker** — Your Quickback-compiled API. Calls `realtime.insert()` etc. after CRUD operations.\n2. **Realtime Worker** — Separate Cloudflare Worker with Durable Object for managing WebSocket connections, one per organization.\n3. **Browser Clients** — Connect via WebSocket, receive filtered and masked broadcasts.\n\n## Key Features\n\n| Feature | Description |\n|---------|-------------|\n| Organization-scoped | Each org gets its own Durable Object instance |\n| Role-based filtering | Only send events to users with matching roles |\n| Per-role masking | Different users see different field values based on their role |\n| User-specific targeting | Send events to a specific user within an org |\n| Custom broadcasts | Arbitrary events beyond CRUD |\n| Custom namespaces | `defineRealtime()` for type-safe event helpers |\n| Two auth methods | Session tokens (browser) and API keys (server/CLI) |\n\n## Enabling Realtime\n\nAdd `realtime` to individual table definitions:\n\n```typescript\nexport default defineTable(applications, {\n firewall: { organization: {} },\n realtime: {\n enabled: true,\n onInsert: true,\n onUpdate: true,\n onDelete: true,\n requiredRoles: [\"recruiter\", \"hiring-manager\"],\n fields: [\"id\", \"candidateId\", \"stage\"],\n },\n});\n```\n\nAnd enable the realtime binding in your database config. The compiler generates the Durable Object worker and helper functions.\n\n## Pages\n\n- **[Durable Objects Setup](/stack/realtime/durable-objects)** — Configuration, wrangler bindings, event formats, masking, and custom namespaces\n- **[Using Realtime](/stack/realtime/using-realtime)** — WebSocket connection, authentication, and client-side handling"
351
363
  },
352
364
  "stack/realtime/using-realtime": {
353
365
  "title": "Using Realtime",
354
- "content": "This page covers connecting to the Quickback realtime system from client applications — authentication, subscribing to events, and handling messages.\n\n## Connecting\n\nOpen a WebSocket connection to the realtime worker:\n\n```typescript\nconst ws = new WebSocket(\"wss://api.yourdomain.com/realtime/v1/websocket\");\n```\n\n## Authentication\n\nAfter connecting, send an auth message to authenticate. Two methods are supported.\n\n### Session Token (Browser/App)\n\n```typescript\nws.onopen = () => {\n ws.send(JSON.stringify({\n type: \"auth\",\n token: sessionToken, // JWT from Better Auth session\n organizationId: activeOrgId,\n }));\n};\n```\n\n### API Key (Server/CLI)\n\n```typescript\nws.onopen = () => {\n ws.send(JSON.stringify({\n type: \"auth\",\n token: apiKey, // API key for machine-to-machine auth\n organizationId: orgId,\n }));\n};\n```\n\n### Auth Response\n\nOn success:\n\n```json\n{\n \"type\": \"auth_success\",\n \"organizationId\": \"org_123\",\n \"userId\": \"user_456\",\n \"role\": \"admin\",\n \"roles\": [\"admin\", \"member\"],\n \"authMethod\": \"session\"\n}\n```\n\nOn failure, the connection is closed with an error message.\n\n## Handling Messages\n\n### CRUD Events\n\nCRUD events use the `postgres_changes` type:\n\n```typescript\nws.onmessage = (event) => {\n const msg = JSON.parse(event.data);\n\n if (msg.type === \"postgres_changes\") {\n const { table, eventType, new: newRecord, old: oldRecord } = msg;\n\n switch (eventType) {\n case \"INSERT\":\n addRecord(table, newRecord);\n break;\n case \"UPDATE\":\n updateRecord(table, newRecord);\n break;\n case \"DELETE\":\n removeRecord(table, oldRecord.id);\n break;\n }\n }\n};\n```\n\n**Event payload:**\n\n```json\n{\n \"type\": \"postgres_changes\",\n \"table\": \"claims\",\n \"schema\": \"public\",\n \"eventType\": \"INSERT\",\n \"new\": { \"id\": \"clm_123\", \"title\": \"Breaking News\", \"status\": \"pending\" },\n \"old\": null\n}\n```\n\nFor UPDATE events, both `new` and `old` are populated. For DELETE events, only `old` is populated.\n\n### Custom Broadcasts\n\nCustom events use the `broadcast` type:\n\n```typescript\nif (msg.type === \"broadcast\") {\n const { event, payload } = msg;\n\n if (event === \"processing-complete\") {\n refreshMaterial(payload.materialId);\n } else if (event === \"extraction:progress\") {\n updateProgressBar(payload.percent);\n }\n}\n```\n\n**Event payload:**\n\n```json\n{\n \"type\": \"broadcast\",\n \"event\": \"processing-complete\",\n \"payload\": {\n \"materialId\": \"mat_123\",\n \"claimCount\": 5,\n \"duration\": 1234\n }\n}\n```\n\nCustom namespaces (from `defineRealtime()`) use the format `namespace:event` — e.g., `extraction:started`, `extraction:progress`.\n\n## Security\n\n### Role-Based Filtering\n\nEvents are only delivered to users whose role matches the broadcast's `targetRoles`. If a table's realtime config specifies `requiredRoles: [\"admin\", \"member\"]`, only users with those roles receive the events.\n\n### Per-Role Masking\n\nField values are masked according to the subscriber's role. For example, with this masking config:\n\n```typescript\nmasking: {\n ssn: { type: \"ssn\", show: { roles: [\"admin\"] } },\n}\n```\n\n- **Admin sees:** `{ ssn: \"123-45-6789\" }`\n- **Member sees:** `{ ssn: \"*****6789\" }`\n\nMasking is pre-computed per-role (O(roles), not O(subscribers)) for efficiency.\n\n### User-Specific Events\n\nEvents can target a specific user within an organization. Only that user receives the broadcast — all other connections in the org are skipped.\n\n### Organization Isolation\n\nEach organization has its own Durable Object instance. Users can only subscribe to events in organizations they belong to, enforced during authentication.\n\n## Reconnection\n\nWebSocket connections can drop due to network issues. Implement reconnection logic in your client:\n\n```typescript\nfunction connect() {\n const ws = new WebSocket(\"wss://api.yourdomain.com/realtime/v1/websocket\");\n\n ws.onopen = () => {\n ws.send(JSON.stringify({\n type: \"auth\",\n token: sessionToken,\n organizationId: activeOrgId,\n }));\n };\n\n ws.onclose = () => {\n // Reconnect after delay\n setTimeout(connect, 2000);\n };\n\n ws.onmessage = (event) => {\n const msg = JSON.parse(event.data);\n handleMessage(msg);\n };\n\n return ws;\n}\n```\n\n## Required Environment Variables\n\n| Variable | Description |\n|----------|-------------|\n| `REALTIME_URL` | URL of the broadcast/realtime worker |\n| `ACCESS_TOKEN` | Internal service-to-service auth token |\n\n```toml\n# wrangler.toml\n[vars]\nREALTIME_URL = \"https://my-app-broadcast.workers.dev\"\nACCESS_TOKEN = \"your-internal-secret-token\"\n```\n\n## Cloudflare Only\n\nRealtime requires Cloudflare Durable Objects and is only available with the Cloudflare runtime.\n\n## See Also\n\n- [Durable Objects Setup](/stack/realtime/durable-objects) — Configuration, event formats, masking, and custom namespaces\n- [Masking](/compiler/definitions/masking) — Field masking configuration"
366
+ "content": "This page covers connecting to the Quickback realtime system from client applications — authentication, subscribing to events, and handling messages.\n\n## Connecting\n\nOpen a WebSocket connection to the realtime worker:\n\n```typescript\nconst ws = new WebSocket(\"wss://api.yourdomain.com/realtime/v1/websocket\");\n```\n\n## Authentication\n\nAfter connecting, send an auth message to authenticate. Two methods are supported.\n\n### Session Token (Browser/App)\n\n```typescript\nws.onopen = () => {\n ws.send(JSON.stringify({\n type: \"auth\",\n token: sessionToken, // JWT from Better Auth session\n organizationId: activeOrgId,\n }));\n};\n```\n\n### API Key (Server/CLI)\n\n```typescript\nws.onopen = () => {\n ws.send(JSON.stringify({\n type: \"auth\",\n token: apiKey, // API key for machine-to-machine auth\n organizationId: orgId,\n }));\n};\n```\n\n### Auth Response\n\nOn success:\n\n```json\n{\n \"type\": \"auth_success\",\n \"organizationId\": \"org_123\",\n \"userId\": \"user_456\",\n \"role\": \"hiring-manager\",\n \"roles\": [\"hiring-manager\", \"recruiter\"],\n \"authMethod\": \"session\"\n}\n```\n\nOn failure, the connection is closed with an error message.\n\n## Handling Messages\n\n### CRUD Events\n\nCRUD events use the `postgres_changes` type:\n\n```typescript\nws.onmessage = (event) => {\n const msg = JSON.parse(event.data);\n\n if (msg.type === \"postgres_changes\") {\n const { table, eventType, new: newRecord, old: oldRecord } = msg;\n\n switch (eventType) {\n case \"INSERT\":\n addRecord(table, newRecord);\n break;\n case \"UPDATE\":\n updateRecord(table, newRecord);\n break;\n case \"DELETE\":\n removeRecord(table, oldRecord.id);\n break;\n }\n }\n};\n```\n\n**Event payload:**\n\n```json\n{\n \"type\": \"postgres_changes\",\n \"table\": \"applications\",\n \"schema\": \"public\",\n \"eventType\": \"INSERT\",\n \"new\": { \"id\": \"app_123\", \"candidateId\": \"cnd_456\", \"stage\": \"interview\" },\n \"old\": null\n}\n```\n\nFor UPDATE events, both `new` and `old` are populated. For DELETE events, only `old` is populated.\n\n### Custom Broadcasts\n\nCustom events use the `broadcast` type:\n\n```typescript\nif (msg.type === \"broadcast\") {\n const { event, payload } = msg;\n\n if (event === \"screening-complete\") {\n refreshApplication(payload.applicationId);\n } else if (event === \"screening:progress\") {\n updateProgressBar(payload.percent);\n }\n}\n```\n\n**Event payload:**\n\n```json\n{\n \"type\": \"broadcast\",\n \"event\": \"screening-complete\",\n \"payload\": {\n \"applicationId\": \"app_123\",\n \"candidateId\": \"cnd_456\",\n \"stage\": \"interview\"\n }\n}\n```\n\nCustom namespaces (from `defineRealtime()`) use the format `namespace:event` — e.g., `screening:started`, `screening:progress`.\n\n## Security\n\n### Role-Based Filtering\n\nEvents are only delivered to users whose role matches the broadcast's `targetRoles`. If a table's realtime config specifies `requiredRoles: [\"hiring-manager\", \"recruiter\"]`, only users with those roles receive the events.\n\n### Per-Role Masking\n\nField values are masked according to the subscriber's role. For example, with this masking config:\n\n```typescript\nmasking: {\n ssn: { type: \"ssn\", show: { roles: [\"owner\"] } },\n}\n```\n\n- **Owner sees:** `{ ssn: \"123-45-6789\" }`\n- **Recruiter sees:** `{ ssn: \"*****6789\" }`\n\nMasking is pre-computed per-role (O(roles), not O(subscribers)) for efficiency.\n\n### User-Specific Events\n\nEvents can target a specific user within an organization. Only that user receives the broadcast — all other connections in the org are skipped.\n\n### Organization Isolation\n\nEach organization has its own Durable Object instance. Users can only subscribe to events in organizations they belong to, enforced during authentication.\n\n## Reconnection\n\nWebSocket connections can drop due to network issues. Implement reconnection logic in your client:\n\n```typescript\nfunction connect() {\n const ws = new WebSocket(\"wss://api.yourdomain.com/realtime/v1/websocket\");\n\n ws.onopen = () => {\n ws.send(JSON.stringify({\n type: \"auth\",\n token: sessionToken,\n organizationId: activeOrgId,\n }));\n };\n\n ws.onclose = () => {\n // Reconnect after delay\n setTimeout(connect, 2000);\n };\n\n ws.onmessage = (event) => {\n const msg = JSON.parse(event.data);\n handleMessage(msg);\n };\n\n return ws;\n}\n```\n\n## Required Environment Variables\n\n| Variable | Description |\n|----------|-------------|\n| `REALTIME_URL` | URL of the broadcast/realtime worker |\n| `ACCESS_TOKEN` | Internal service-to-service auth token |\n\n```toml\n# wrangler.toml\n[vars]\nREALTIME_URL = \"https://my-app-broadcast.workers.dev\"\nACCESS_TOKEN = \"your-internal-secret-token\"\n```\n\n## Cloudflare Only\n\nRealtime requires Cloudflare Durable Objects and is only available with the Cloudflare runtime.\n\n## See Also\n\n- [Durable Objects Setup](/stack/realtime/durable-objects) — Configuration, event formats, masking, and custom namespaces\n- [Masking](/compiler/definitions/masking) — Field masking configuration"
355
367
  },
356
368
  "stack/storage": {
357
369
  "title": "Storage",
@@ -367,15 +379,15 @@ export const DOCS = {
367
379
  },
368
380
  "stack/storage/using-kv": {
369
381
  "title": "Using KV",
370
- "content": "Cloudflare KV is used by the Quickback Stack for session storage, caching, and rate limiting.\n\n## Session Storage\n\nBetter Auth stores sessions in KV by default for fast edge-based lookups:\n\n```typescript\n// KV session adapter (auto-configured)\nconst session = await kv.get(`session:${sessionId}`);\n```\n\n## Rate Limiting\n\nKV powers the rate limiting middleware:\n\n```typescript\n// Rate limit key structure\nconst key = `ratelimit:${ip}:${endpoint}`;\n```\n\n## Caching\n\nUse KV for caching frequently accessed data with configurable TTL.\n\n## Related\n\n- [KV Setup](/stack/storage/kv) — Namespace and bindings configuration\n- [Auth Security](/stack/auth/security) — Rate limiting configuration"
382
+ "content": "Cloudflare KV is used by the Quickback Stack for session storage, caching, and rate limiting.\n\n## Session Storage\n\nBetter Auth stores sessions in KV by default for fast edge-based lookups. This is auto-configured by the compiler — no setup needed.\n\n```typescript\n// How sessions work internally (auto-generated)\n// On login: session stored in KV\nawait kv.put(`session:${sessionToken}`, JSON.stringify(sessionData), {\n expirationTtl: 60 * 60 * 24 * 7, // 7 days\n});\n\n// On each request: session retrieved from KV\nconst session = await kv.get(`session:${sessionToken}`, \"json\");\n```\n\nSessions are automatically:\n- **Created** on sign-in (email/password, passkey, magic link, etc.)\n- **Validated** on every authenticated request via the `Authorization: Bearer <token>` header\n- **Expired** after the configured TTL (default: 7 days)\n- **Deleted** on sign-out\n\n## Rate Limiting\n\nKV powers the rate limiting middleware that protects auth endpoints:\n\n```typescript\n// Rate limit key structure (auto-generated)\n// Key: ratelimit:{ip}:{endpoint}\n// Value: request count\n// TTL: rate limit window (e.g., 60 seconds)\nconst key = `ratelimit:${ip}:${endpoint}`;\nconst count = await kv.get(key);\nif (count && parseInt(count) > limit) {\n return c.json({ error: \"Too many requests\" }, 429);\n}\n```\n\nSee [Auth Security](/stack/auth/security) for rate limiting configuration options.\n\n## Caching\n\nYou can use KV in custom action handlers for caching frequently accessed data:\n\n```typescript\n// In an action handler\nexecute: async ({ db, ctx }) => {\n const kv = ctx.env.KV;\n\n // Check cache first\n const cached = await kv.get(\"expensive-query-result\", \"json\");\n if (cached) return cached;\n\n // Compute and cache\n const result = await db.select().from(analytics);\n await kv.put(\"expensive-query-result\", JSON.stringify(result), {\n expirationTtl: 300, // 5 minutes\n });\n\n return result;\n}\n```\n\n## See Also\n\n- [KV Setup](/stack/storage/kv) — Namespace creation and wrangler bindings\n- [Auth Security](/stack/auth/security) — Rate limiting and cookie security configuration"
371
383
  },
372
384
  "stack/storage/using-r2": {
373
385
  "title": "Using R2",
374
- "content": "Cloudflare R2 provides S3-compatible object storage for file uploads in the Quickback Stack.\n\n## Upload Flow\n\nThe generated API includes upload endpoints:\n\n1. Client requests a presigned upload URL\n2. Client uploads directly to R2\n3. API stores the file reference in D1\n\n## Download Flow\n\nFiles can be accessed via the generated download endpoint with role-based access control.\n\n## Role-Based Access\n\nFile access respects the same security layers as your API:\n\n- **Firewall** — Users can only access files belonging to their organization\n- **Access** — Role-based download permissions\n\n## Related\n\n- [R2 Setup](/stack/storage/r2) — Bucket and bindings configuration\n- [Avatars](/account-ui/features/avatars) — Avatar upload UI"
386
+ "content": "Cloudflare R2 provides S3-compatible object storage for file uploads in the Quickback Stack.\n\n## Upload Flow\n\nThe generated API provides a two-step upload process:\n\n1. **Request a presigned URL** — Client calls the upload endpoint to get a time-limited URL\n2. **Upload directly to R2** — Client uploads the file directly to R2 (bypasses your Worker)\n3. **Store the reference** — The API records the file metadata in D1\n\n```bash\n# 1. Request presigned upload URL\ncurl -X POST https://api.example.com/api/v1/files/upload \\\n -H \"Authorization: Bearer <token>\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"filename\": \"report.pdf\", \"contentType\": \"application/pdf\"}'\n\n# Response:\n# { \"uploadUrl\": \"https://bucket.r2.cloudflarestorage.com/...\", \"fileId\": \"file_abc123\" }\n\n# 2. Upload directly to R2 using the presigned URL\ncurl -X PUT \"<uploadUrl>\" \\\n -H \"Content-Type: application/pdf\" \\\n --data-binary @report.pdf\n```\n\n## Download Flow\n\nFiles are served through your Worker with security checks applied:\n\n```bash\n# Download a file (auth + firewall enforced)\ncurl https://api.example.com/api/v1/files/file_abc123/download \\\n -H \"Authorization: Bearer <token>\"\n```\n\nThe download endpoint:\n1. Validates the user's session\n2. Checks firewall — the file must belong to the user's organization\n3. Checks access — the user must have the required role\n4. Streams the file from R2\n\n## Role-Based Access\n\nFile access respects the same security layers as your API:\n\n- **Firewall** — Users can only access files belonging to their organization\n- **Access** — Role-based download permissions\n\n## See Also\n\n- [R2 Setup](/stack/storage/r2) — Bucket creation, wrangler bindings, and configuration\n- [Avatars](/account-ui/features/avatars) — Avatar upload UI integration"
375
387
  },
376
388
  "stack/vector/embeddings": {
377
389
  "title": "Automatic Embeddings",
378
- "content": "Quickback can automatically generate embeddings for your data using Cloudflare Queues and Workers AI. When configured, INSERT and UPDATE operations automatically enqueue embedding jobs that are processed asynchronously.\n\n## Enabling Embeddings\n\nAdd an `embeddings` configuration to your resource definition:\n\n```typescript\n// claims/resource.ts\n\nexport default defineResource(claims, {\n firewall: { organization: {} },\n\n embeddings: {\n fields: ['content'], // Fields to concatenate and embed\n model: '@cf/baai/bge-base-en-v1.5', // Embedding model (optional)\n onInsert: true, // Auto-embed on create (default: true)\n onUpdate: ['content'], // Re-embed when these fields change\n embeddingColumn: 'embedding', // Column to store embedding\n metadata: ['storyId'], // Metadata for Vectorize index\n },\n\n crud: {\n create: { access: { roles: ['member'] } },\n update: { access: { roles: ['member'] } },\n },\n});\n```\n\n## Configuration Options\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `fields` | `string[]` | **Required** | Fields to concatenate and embed |\n| `model` | `string` | `'@cf/baai/bge-base-en-v1.5'` | Workers AI embedding model |\n| `onInsert` | `boolean` | `true` | Embed on INSERT operations |\n| `onUpdate` | `boolean \\| string[]` | `true` | Embed on UPDATE; array limits to specific fields |\n| `embeddingColumn` | `string` | `'embedding'` | Column to store the embedding vector |\n| `separator` | `string` | `' '` | Separator for joining multiple fields |\n| `metadata` | `string[]` | `[]` | Fields to include in Vectorize metadata |\n\n### onUpdate Options\n\n```typescript\n// Always re-embed on any update\nonUpdate: true\n\n// Never re-embed on update\nonUpdate: false\n\n// Only re-embed when specific fields change\nonUpdate: ['content', 'title']\n```\n\n## How It Works\n\n```\n┌─────────────────────────────────────────────────────────────────────┐\n│ Main API Worker │\n│ │\n│ ┌─────────────────────┐ ┌────────────────────┐ │\n│ │ POST /claims │ │ Queue Consumer │ │\n│ │ │ │ │ │\n│ │ 1. Auth middleware │ │ 1. Workers AI │ │\n│ │ 2. Firewall │ │ embed() │ │\n│ │ 3. Guards │ │ 2. D1 update() │ │\n│ │ 4. Insert to D1 │ │ 3. Vectorize │ │\n│ │ 5. Enqueue job ─────┼───▶│ upsert() │ │\n│ └─────────────────────┘ └────────────────────┘ │\n│ ▲ │\n│ ┌─────────────────┴──────────────────┐ │\n│ │ EMBEDDINGS_QUEUE │ │\n│ └────────────────────────────────────┘ │\n└─────────────────────────────────────────────────────────────────────┘\n```\n\n1. **API Request** - POST/PATCH arrives and passes through auth, firewall, guards\n2. **Database Insert** - Record is created/updated in D1\n3. **Enqueue Job** - Embedding job is sent to Cloudflare Queue\n4. **Queue Consumer** - Processes job asynchronously:\n - Calls Workers AI to generate embedding\n - Updates D1 with embedding vector\n - Optionally upserts to Vectorize index\n\n## Security Model\n\n**Security is enforced at enqueue time, not consume time:**\n\n- Jobs are only enqueued after passing all security checks (auth, firewall, guards)\n- The queue consumer is an internal process that executes pre-validated jobs\n- If a user can't create a claim, they can't trigger an embedding job\n\n## Generic Embeddings API\n\nIn addition to automatic embeddings on CRUD operations, Quickback generates a generic embeddings API endpoint that allows you to trigger embeddings on arbitrary content.\n\n### POST /api/v1/embeddings\n\nGenerate an embedding for any text content:\n\n```bash\ncurl -X POST https://your-api.workers.dev/api/v1/embeddings \\\n -H \"Content-Type: application/json\" \\\n -H \"Cookie: better-auth.session_token=...\" \\\n -d '{\n \"content\": \"Text to embed\",\n \"model\": \"@cf/baai/bge-base-en-v1.5\",\n \"table\": \"claims\",\n \"id\": \"clm_123\"\n }'\n```\n\n#### Request Body\n\n| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n| `content` | `string` | Yes | Text to generate embedding for |\n| `model` | `string` | No | Embedding model (defaults to table config or `@cf/baai/bge-base-en-v1.5`) |\n| `table` | `string` | No | Table to store embedding back to |\n| `id` | `string` | Conditional | Record ID to update (required if `table` is specified) |\n\n#### Response\n\n```json\n{\n \"queued\": true,\n \"jobId\": \"550e8400-e29b-41d4-a716-446655440000\",\n \"table\": \"claims\",\n \"id\": \"clm_123\",\n \"model\": \"@cf/baai/bge-base-en-v1.5\"\n}\n```\n\n### GET /api/v1/embeddings/tables\n\nList tables that have embeddings configured:\n\n```bash\ncurl https://your-api.workers.dev/api/v1/embeddings/tables \\\n -H \"Cookie: better-auth.session_token=...\"\n```\n\nResponse:\n\n```json\n{\n \"tables\": [\n {\n \"name\": \"claims\",\n \"embeddingColumn\": \"embedding\",\n \"model\": \"@cf/baai/bge-base-en-v1.5\"\n }\n ]\n}\n```\n\n### Authentication & Authorization\n\nThe generic embeddings API requires authentication and uses the `activeOrgId` from the user's context to enforce organization-level isolation. Embedding jobs are scoped to the user's current organization.\n\n### Use Cases\n\nThe generic embeddings API is useful for:\n\n- **Batch embedding**: Embed content without going through CRUD routes\n- **Re-embedding**: Force re-generation of embeddings for existing records\n- **Preview embeddings**: Test embedding generation before persisting\n- **External content**: Embed content that doesn't fit your defined schemas\n\n## Generated Files\n\nWhen embeddings are configured, the compiler generates:\n\n| File | Purpose |\n|------|---------|\n| `src/queue-consumer.ts` | Queue consumer handler for processing embedding jobs |\n| `src/routes/embeddings.ts` | Generic embeddings API routes |\n| `wrangler.toml` | Queue producer/consumer bindings, AI binding |\n| `src/env.d.ts` | `EMBEDDINGS_QUEUE`, `AI` types |\n| `src/index.ts` | Exports `queue` handler, mounts `/api/v1/embeddings` |\n\n### wrangler.toml additions\n\n```toml\n# Embeddings Queue\n[[queues.producers]]\nqueue = \"your-app-embeddings-queue\"\nbinding = \"EMBEDDINGS_QUEUE\"\n\n[[queues.consumers]]\nqueue = \"your-app-embeddings-queue\"\nmax_batch_size = 10\nmax_batch_timeout = 30\nmax_retries = 3\n\n# Workers AI\n[ai]\nbinding = \"AI\"\n```\n\n## Multiple Fields\n\nEmbed multiple fields by concatenating them:\n\n```typescript\nembeddings: {\n fields: ['title', 'content', 'summary'], // Joined with spaces by default\n // ...\n}\n```\n\nGenerated embedding text: `\"${title} ${content} ${summary}\"`\n\n### Custom Separator\n\nUse `separator` to control how fields are joined. This is useful for sentence boundary detection:\n\n```typescript\nembeddings: {\n fields: ['title', 'summary'],\n separator: '. ', // Join with period + space\n // ...\n}\n```\n\nGenerated code: `[result[0].title, result[0].summary].filter(Boolean).join('. ')`\n\nThe `filter(Boolean)` ensures null or empty fields are excluded cleanly — no trailing separator when a field is absent.\n\n## Vectorize Integration\n\nIf you have a Vectorize index configured, embeddings are automatically upserted:\n\n```typescript\n// quickback.config.ts\nproviders: {\n database: {\n config: {\n vectorizeIndexName: 'claims-embeddings', // Your Vectorize index\n vectorizeBinding: 'VECTORIZE',\n },\n },\n},\n```\n\nThe queue consumer will:\n1. Generate the embedding via Workers AI\n2. Store the vector in D1 (JSON string)\n3. Upsert to Vectorize with metadata\n\n### Vectorize Metadata\n\nInclude fields in Vectorize metadata for filtering:\n\n```typescript\nembeddings: {\n fields: ['content'],\n metadata: ['storyId', 'organizationId', 'claimType'],\n}\n```\n\nEnables queries like:\n```typescript\nconst results = await env.VECTORIZE.query(vector, {\n topK: 10,\n filter: { storyId: 'story_123' }\n});\n```\n\n## Schema Requirements\n\nYour schema must include the embedding column:\n\n```typescript\n// claims/schema.ts\n\nexport const claims = sqliteTable(\"claims\", {\n id: text(\"id\").primaryKey(),\n content: text(\"content\").notNull(),\n storyId: text(\"story_id\"),\n organizationId: text(\"organization_id\").notNull(),\n\n // Embedding column - stores JSON array of floats\n embedding: text(\"embedding\"),\n});\n```\n\n## Supported Models\n\nAny Workers AI embedding model can be used:\n\n| Model | Dimensions | Notes |\n|-------|------------|-------|\n| `@cf/baai/bge-base-en-v1.5` | 768 | Default, good general-purpose |\n| `@cf/baai/bge-small-en-v1.5` | 384 | Faster, smaller |\n| `@cf/baai/bge-large-en-v1.5` | 1024 | Higher quality |\n\nSee [Workers AI Models](https://developers.cloudflare.com/workers-ai/models/) for the full list.\n\n## Deployment\n\nAfter compiling with embeddings:\n\n1. **Create the queue:**\n ```bash\n wrangler queues create your-app-embeddings-queue\n ```\n\n2. **Deploy the worker:**\n ```bash\n wrangler deploy\n ```\n\nThe single worker handles both HTTP requests and queue consumption.\n\n## Monitoring\n\nQueue metrics are available in the Cloudflare dashboard:\n- Messages enqueued\n- Messages processed\n- Retry count\n- Consumer lag\n\nCheck queue health:\n```bash\nwrangler queues list\nwrangler queues consumer your-app-embeddings-queue\n```\n\n## Error Handling\n\nFailed jobs are automatically retried (up to `max_retries`):\n\n```typescript\n// In queue consumer\ntry {\n const embedding = await env.AI.run(job.model, { text: job.content });\n // ...\n message.ack();\n} catch (error) {\n console.error('[Queue] Embedding job failed:', error);\n message.retry(); // Will retry up to 3 times\n}\n```\n\nAfter max retries, the message is dead-lettered (if configured) or dropped.\n\n## Similarity Search Service\n\nFor applications that need typed similarity search with classification, you can define embedding search configurations using `defineEmbedding`. This generates a service layer with typed search functions.\n\n### Defining Search Configurations\n\nCreate a file in `services/embeddings/`:\n\n```typescript\n// services/embeddings/claim-similarity.ts\n\nexport default defineEmbedding({\n name: 'claim-similarity',\n description: 'Find similar claims with classification',\n\n // Source configuration\n source: 'claims', // Table name\n vectorIndex: 'VECTORIZE', // Binding name\n model: '@cf/baai/bge-base-en-v1.5',\n\n // Search configuration\n search: {\n threshold: 0.60, // Minimum similarity (default: 0.60)\n limit: 10, // Max results (default: 10)\n classify: {\n DUPLICATE: 0.90, // Score >= 0.90 = DUPLICATE\n CONFIRMS: 0.85, // Score >= 0.85 = CONFIRMS\n RELATED: 0.75, // Score >= 0.75 = RELATED\n },\n filters: ['storyId', 'organizationId'], // Filterable fields\n },\n\n // Generation triggers (beyond CRUD)\n triggers: {\n onQueueMessage: 'embed_claim', // Listen for queue messages\n },\n});\n```\n\n### Generated Service Layer\n\nAfter compilation, a `createEmbeddings()` helper is generated in `src/lib/embeddings.ts`:\n\n```typescript\n\nexport const execute: ActionExecutor = async ({ ctx, input }) => {\n const embeddings = createEmbeddings(ctx.env);\n\n // Search with automatic classification\n const similar = await embeddings.claimSimilarity.search(\n 'Police arrested three suspects in downtown robbery',\n {\n storyId: 'story_123',\n limit: 5,\n threshold: 0.70,\n }\n );\n\n // Returns: [{ id, score: 0.87, classification: 'CONFIRMS', metadata }]\n for (const match of similar) {\n console.log(`${match.classification}: ${match.id} (${match.score})`);\n }\n\n return { similar };\n};\n```\n\n### Classification Thresholds\n\nResults are automatically classified based on similarity score:\n\n| Classification | Default Threshold | Meaning |\n|----------------|-------------------|---------|\n| `DUPLICATE` | >= 0.90 | Near-identical content |\n| `CONFIRMS` | >= 0.85 | Strongly supports same claim |\n| `RELATED` | >= 0.75 | Topically related |\n| `NEW` | < 0.75 | No significant match |\n\nCustomize thresholds per use case:\n\n```typescript\nsearch: {\n classify: {\n DUPLICATE: 0.95, // Stricter duplicate detection\n CONFIRMS: 0.88,\n RELATED: 0.70, // Broader \"related\" category\n },\n}\n```\n\n### Gray Zone Detection\n\nFor cases where automatic classification isn't sufficient, use gray zone detection to get matches that need semantic evaluation:\n\n```typescript\nconst results = await embeddings.claimSimilarity.findWithGrayZone(\n 'Some claim text',\n { min: 0.60, max: 0.85 }\n);\n\n// Returns structured results:\n// {\n// high_confidence: [...], // Score >= 0.85 (auto-classified)\n// gray_zone: [...] // 0.60 <= score < 0.85 (needs review)\n// }\n\n// Process high confidence matches automatically\nfor (const match of results.high_confidence) {\n await markAsDuplicate(match.id);\n}\n\n// Queue gray zone for semantic review\nfor (const match of results.gray_zone) {\n await queueForReview(match.id, match.score);\n}\n```\n\n### Generate Embeddings Directly\n\nGenerate embeddings without searching:\n\n```typescript\nconst embeddings = createEmbeddings(ctx.env);\n\n// Get raw embedding vector\nconst vector = await embeddings.claimSimilarity.embed(\n 'Text to embed'\n);\n// Returns: number[] (768 dimensions for bge-base)\n```\n\n### Multiple Search Configurations\n\nDefine different configurations for different use cases:\n\n```typescript\n// services/embeddings/story-similarity.ts\nexport default defineEmbedding({\n name: 'story-similarity',\n source: 'stories',\n search: {\n threshold: 0.65,\n limit: 20,\n classify: {\n DUPLICATE: 0.92,\n CONFIRMS: 0.80,\n RELATED: 0.65,\n },\n filters: ['streamId'],\n },\n});\n\n// services/embeddings/material-similarity.ts\nexport default defineEmbedding({\n name: 'material-similarity',\n source: 'materials',\n search: {\n threshold: 0.70,\n limit: 5,\n classify: {\n DUPLICATE: 0.95,\n CONFIRMS: 0.90,\n RELATED: 0.80,\n },\n filters: ['sourceType', 'organizationId'],\n },\n});\n```\n\nUsage:\n```typescript\nconst embeddings = createEmbeddings(ctx.env);\n\n// Different search behaviors for different content types\nconst similarClaims = await embeddings.claimSimilarity.search(text, opts);\nconst similarStories = await embeddings.storySimilarity.search(text, opts);\nconst similarMaterials = await embeddings.materialSimilarity.search(text, opts);\n```\n\n### Table-Level vs Service-Level\n\n| Feature | Table-level (`defineTable`) | Service-level (`defineEmbedding`) |\n|---------|----------------------------|----------------------------------|\n| Auto-embed on INSERT | ✅ | ❌ |\n| Auto-embed on UPDATE | ✅ | ❌ |\n| Custom search functions | ❌ | ✅ |\n| Classification thresholds | ❌ | ✅ |\n| Gray zone detection | ❌ | ✅ |\n| Filterable searches | ❌ | ✅ |\n| Queue message triggers | ❌ | ✅ |\n\n**Use both together:**\n- `defineTable` with `embeddings` config for automatic embedding generation\n- `defineEmbedding` for typed search functions with classification"
390
+ "content": "Quickback can automatically generate embeddings for your data using Cloudflare Queues and Workers AI. When configured, INSERT and UPDATE operations automatically enqueue embedding jobs that are processed asynchronously.\n\n## Enabling Embeddings\n\nAdd an `embeddings` configuration to your resource definition:\n\n```typescript\n// quickback/features/jobs/jobs.ts\n\nexport default defineTable(jobs, {\n firewall: { organization: {} },\n\n embeddings: {\n fields: ['description'], // Fields to concatenate and embed\n model: '@cf/baai/bge-base-en-v1.5', // Embedding model (optional)\n onInsert: true, // Auto-embed on create (default: true)\n onUpdate: ['description'], // Re-embed when these fields change\n embeddingColumn: 'embedding', // Column to store embedding\n metadata: ['department'], // Metadata for Vectorize index\n },\n\n crud: {\n create: { access: { roles: ['recruiter'] } },\n update: { access: { roles: ['recruiter'] } },\n },\n});\n```\n\n## Configuration Options\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `fields` | `string[]` | **Required** | Fields to concatenate and embed |\n| `model` | `string` | `'@cf/baai/bge-base-en-v1.5'` | Workers AI embedding model |\n| `onInsert` | `boolean` | `true` | Embed on INSERT operations |\n| `onUpdate` | `boolean \\| string[]` | `true` | Embed on UPDATE; array limits to specific fields |\n| `embeddingColumn` | `string` | `'embedding'` | Column to store the embedding vector |\n| `separator` | `string` | `' '` | Separator for joining multiple fields |\n| `metadata` | `string[]` | `[]` | Fields to include in Vectorize metadata |\n\n### onUpdate Options\n\n```typescript\n// Always re-embed on any update\nonUpdate: true\n\n// Never re-embed on update\nonUpdate: false\n\n// Only re-embed when specific fields change\nonUpdate: ['description', 'title']\n```\n\n## How It Works\n\n```\n┌─────────────────────────────────────────────────────────────────────┐\n│ Main API Worker │\n│ │\n│ ┌─────────────────────┐ ┌────────────────────┐ │\n│ │ POST /jobs │ │ Queue Consumer │ │\n│ │ │ │ │ │\n│ │ 1. Auth middleware │ │ 1. Workers AI │ │\n│ │ 2. Firewall │ │ embed() │ │\n│ │ 3. Guards │ │ 2. D1 update() │ │\n│ │ 4. Insert to D1 │ │ 3. Vectorize │ │\n│ │ 5. Enqueue job ─────┼───▶│ upsert() │ │\n│ └─────────────────────┘ └────────────────────┘ │\n│ ▲ │\n│ ┌─────────────────┴──────────────────┐ │\n│ │ EMBEDDINGS_QUEUE │ │\n│ └────────────────────────────────────┘ │\n└─────────────────────────────────────────────────────────────────────┘\n```\n\n1. **API Request** - POST/PATCH arrives and passes through auth, firewall, guards\n2. **Database Insert** - Record is created/updated in D1\n3. **Enqueue Job** - Embedding job is sent to Cloudflare Queue\n4. **Queue Consumer** - Processes job asynchronously:\n - Calls Workers AI to generate embedding\n - Updates D1 with embedding vector\n - Optionally upserts to Vectorize index\n\n## Security Model\n\n**Security is enforced at enqueue time, not consume time:**\n\n- Jobs are only enqueued after passing all security checks (auth, firewall, guards)\n- The queue consumer is an internal process that executes pre-validated jobs\n- If a user can't create a job posting, they can't trigger an embedding job\n\n## Generic Embeddings API\n\nIn addition to automatic embeddings on CRUD operations, Quickback generates a generic embeddings API endpoint that allows you to trigger embeddings on arbitrary content.\n\n### POST /api/v1/embeddings\n\nGenerate an embedding for any text content:\n\n```bash\ncurl -X POST https://your-api.workers.dev/api/v1/embeddings \\\n -H \"Content-Type: application/json\" \\\n -H \"Cookie: better-auth.session_token=...\" \\\n -d '{\n \"content\": \"Text to embed\",\n \"model\": \"@cf/baai/bge-base-en-v1.5\",\n \"table\": \"jobs\",\n \"id\": \"job_123\"\n }'\n```\n\n#### Request Body\n\n| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n| `content` | `string` | Yes | Text to generate embedding for |\n| `model` | `string` | No | Embedding model (defaults to table config or `@cf/baai/bge-base-en-v1.5`) |\n| `table` | `string` | No | Table to store embedding back to |\n| `id` | `string` | Conditional | Record ID to update (required if `table` is specified) |\n\n#### Response\n\n```json\n{\n \"queued\": true,\n \"jobId\": \"550e8400-e29b-41d4-a716-446655440000\",\n \"table\": \"jobs\",\n \"id\": \"job_123\",\n \"model\": \"@cf/baai/bge-base-en-v1.5\"\n}\n```\n\n### GET /api/v1/embeddings/tables\n\nList tables that have embeddings configured:\n\n```bash\ncurl https://your-api.workers.dev/api/v1/embeddings/tables \\\n -H \"Cookie: better-auth.session_token=...\"\n```\n\nResponse:\n\n```json\n{\n \"tables\": [\n {\n \"name\": \"jobs\",\n \"embeddingColumn\": \"embedding\",\n \"model\": \"@cf/baai/bge-base-en-v1.5\"\n }\n ]\n}\n```\n\n### Authentication & Authorization\n\nThe generic embeddings API requires authentication and uses the `activeOrgId` from the user's context to enforce organization-level isolation. Embedding jobs are scoped to the user's current organization.\n\n### Use Cases\n\nThe generic embeddings API is useful for:\n\n- **Batch embedding**: Embed content without going through CRUD routes\n- **Re-embedding**: Force re-generation of embeddings for existing records\n- **Preview embeddings**: Test embedding generation before persisting\n- **External content**: Embed content that doesn't fit your defined schemas\n\n## Generated Files\n\nWhen embeddings are configured, the compiler generates:\n\n| File | Purpose |\n|------|---------|\n| `src/queue-consumer.ts` | Queue consumer handler for processing embedding jobs |\n| `src/routes/embeddings.ts` | Generic embeddings API routes |\n| `wrangler.toml` | Queue producer/consumer bindings, AI binding |\n| `src/env.d.ts` | `EMBEDDINGS_QUEUE`, `AI` types |\n| `src/index.ts` | Exports `queue` handler, mounts `/api/v1/embeddings` |\n\n### wrangler.toml additions\n\n```toml\n# Embeddings Queue\n[[queues.producers]]\nqueue = \"your-app-embeddings-queue\"\nbinding = \"EMBEDDINGS_QUEUE\"\n\n[[queues.consumers]]\nqueue = \"your-app-embeddings-queue\"\nmax_batch_size = 10\nmax_batch_timeout = 30\nmax_retries = 3\n\n# Workers AI\n[ai]\nbinding = \"AI\"\n```\n\n## Multiple Fields\n\nEmbed multiple fields by concatenating them:\n\n```typescript\nembeddings: {\n fields: ['title', 'description', 'department'], // Joined with spaces by default\n // ...\n}\n```\n\nGenerated embedding text: `\"${title} ${description} ${department}\"`\n\n### Custom Separator\n\nUse `separator` to control how fields are joined. This is useful for sentence boundary detection:\n\n```typescript\nembeddings: {\n fields: ['title', 'description'],\n separator: '. ', // Join with period + space\n // ...\n}\n```\n\nGenerated code: `[result[0].title, result[0].description].filter(Boolean).join('. ')`\n\nThe `filter(Boolean)` ensures null or empty fields are excluded cleanly — no trailing separator when a field is absent.\n\n## Vectorize Integration\n\nIf you have a Vectorize index configured, embeddings are automatically upserted:\n\n```typescript\n// quickback.config.ts\nproviders: {\n database: {\n config: {\n vectorizeIndexName: 'jobs-embeddings', // Your Vectorize index\n vectorizeBinding: 'VECTORIZE',\n },\n },\n},\n```\n\nThe queue consumer will:\n1. Generate the embedding via Workers AI\n2. Store the vector in D1 (JSON string)\n3. Upsert to Vectorize with metadata\n\n### Vectorize Metadata\n\nInclude fields in Vectorize metadata for filtering:\n\n```typescript\nembeddings: {\n fields: ['description'],\n metadata: ['department', 'organizationId', 'status'],\n}\n```\n\nEnables queries like:\n```typescript\nconst results = await env.VECTORIZE.query(vector, {\n topK: 10,\n filter: { department: 'engineering' }\n});\n```\n\n## Schema Requirements\n\nYour schema must include the embedding column:\n\n```typescript\n// jobs/schema.ts\n\nexport const jobs = sqliteTable(\"jobs\", {\n id: text(\"id\").primaryKey(),\n title: text(\"title\").notNull(),\n description: text(\"description\").notNull(),\n department: text(\"department\"),\n status: text(\"status\").notNull(),\n organizationId: text(\"organization_id\").notNull(),\n\n // Embedding column - stores JSON array of floats\n embedding: text(\"embedding\"),\n});\n```\n\n## Supported Models\n\nAny Workers AI embedding model can be used:\n\n| Model | Dimensions | Notes |\n|-------|------------|-------|\n| `@cf/baai/bge-base-en-v1.5` | 768 | Default, good general-purpose |\n| `@cf/baai/bge-small-en-v1.5` | 384 | Faster, smaller |\n| `@cf/baai/bge-large-en-v1.5` | 1024 | Higher quality |\n\nSee [Workers AI Models](https://developers.cloudflare.com/workers-ai/models/) for the full list.\n\n## Deployment\n\nAfter compiling with embeddings:\n\n1. **Create the queue:**\n ```bash\n wrangler queues create your-app-embeddings-queue\n ```\n\n2. **Deploy the worker:**\n ```bash\n wrangler deploy\n ```\n\nThe single worker handles both HTTP requests and queue consumption.\n\n## Monitoring\n\nQueue metrics are available in the Cloudflare dashboard:\n- Messages enqueued\n- Messages processed\n- Retry count\n- Consumer lag\n\nCheck queue health:\n```bash\nwrangler queues list\nwrangler queues consumer your-app-embeddings-queue\n```\n\n## Error Handling\n\nFailed jobs are automatically retried (up to `max_retries`):\n\n```typescript\n// In queue consumer\ntry {\n const embedding = await env.AI.run(job.model, { text: job.content });\n // ...\n message.ack();\n} catch (error) {\n console.error('[Queue] Embedding job failed:', error);\n message.retry(); // Will retry up to 3 times\n}\n```\n\nAfter max retries, the message is dead-lettered (if configured) or dropped.\n\n## Similarity Search Service\n\nFor applications that need typed similarity search with classification, you can define embedding search configurations using `defineEmbedding`. This generates a service layer with typed search functions.\n\n### Defining Search Configurations\n\nCreate a file in `services/embeddings/`:\n\n```typescript\n// services/embeddings/job-similarity.ts\n\nexport default defineEmbedding({\n name: 'job-similarity',\n description: 'Find similar job postings by description content',\n\n // Source configuration\n source: 'jobs', // Table name\n vectorIndex: 'VECTORIZE', // Binding name\n model: '@cf/baai/bge-base-en-v1.5',\n\n // Search configuration\n search: {\n threshold: 0.60, // Minimum similarity (default: 0.60)\n limit: 10, // Max results (default: 10)\n classify: {\n DUPLICATE_POSTING: 0.90, // Score >= 0.90 = DUPLICATE_POSTING\n SIMILAR_ROLE: 0.85, // Score >= 0.85 = SIMILAR_ROLE\n RELATED: 0.75, // Score >= 0.75 = RELATED\n },\n filters: ['department', 'organizationId'], // Filterable fields\n },\n\n // Generation triggers (beyond CRUD)\n triggers: {\n onQueueMessage: 'embed_job', // Listen for queue messages\n },\n});\n```\n\n### Generated Service Layer\n\nAfter compilation, a `createEmbeddings()` helper is generated in `src/lib/embeddings.ts`:\n\n```typescript\n\nexport const execute: ActionExecutor = async ({ ctx, input }) => {\n const embeddings = createEmbeddings(ctx.env);\n\n // Search with automatic classification\n const similar = await embeddings.jobSimilarity.search(\n 'Senior Full-Stack Engineer with React and Node.js experience',\n {\n department: 'engineering',\n limit: 5,\n threshold: 0.70,\n }\n );\n\n // Returns: [{ id, score: 0.87, classification: 'SIMILAR_ROLE', metadata }]\n for (const match of similar) {\n console.log(`${match.classification}: ${match.id} (${match.score})`);\n }\n\n return { similar };\n};\n```\n\n### Classification Thresholds\n\nResults are automatically classified based on similarity score:\n\n| Classification | Default Threshold | Meaning |\n|----------------|-------------------|---------|\n| `DUPLICATE_POSTING` | >= 0.90 | Near-identical job posting |\n| `SIMILAR_ROLE` | >= 0.85 | Strongly similar role and requirements |\n| `RELATED` | >= 0.75 | Topically related position |\n| `NEW` | < 0.75 | No significant match |\n\nCustomize thresholds per use case:\n\n```typescript\nsearch: {\n classify: {\n DUPLICATE_POSTING: 0.95, // Stricter duplicate detection\n SIMILAR_ROLE: 0.88,\n RELATED: 0.70, // Broader \"related\" category\n },\n}\n```\n\n### Gray Zone Detection\n\nFor cases where automatic classification isn't sufficient, use gray zone detection to get matches that need semantic evaluation:\n\n```typescript\nconst results = await embeddings.jobSimilarity.findWithGrayZone(\n 'Some job description text',\n { min: 0.60, max: 0.85 }\n);\n\n// Returns structured results:\n// {\n// high_confidence: [...], // Score >= 0.85 (auto-classified)\n// gray_zone: [...] // 0.60 <= score < 0.85 (needs review)\n// }\n\n// Process high confidence matches automatically\nfor (const match of results.high_confidence) {\n await markAsDuplicate(match.id);\n}\n\n// Queue gray zone for manual review\nfor (const match of results.gray_zone) {\n await queueForReview(match.id, match.score);\n}\n```\n\n### Generate Embeddings Directly\n\nGenerate embeddings without searching:\n\n```typescript\nconst embeddings = createEmbeddings(ctx.env);\n\n// Get raw embedding vector\nconst vector = await embeddings.jobSimilarity.embed(\n 'Text to embed'\n);\n// Returns: number[] (768 dimensions for bge-base)\n```\n\n### Multiple Search Configurations\n\nDefine different configurations for different use cases:\n\n```typescript\n// services/embeddings/candidate-similarity.ts\nexport default defineEmbedding({\n name: 'candidate-similarity',\n description: 'Match candidates by resume content',\n source: 'candidates',\n search: {\n threshold: 0.65,\n limit: 20,\n classify: {\n DUPLICATE_POSTING: 0.92,\n SIMILAR_ROLE: 0.80,\n RELATED: 0.65,\n },\n filters: ['source'],\n },\n});\n\n// services/embeddings/application-match.ts\nexport default defineEmbedding({\n name: 'application-match',\n description: 'Match applications to similar job postings',\n source: 'applications',\n search: {\n threshold: 0.70,\n limit: 5,\n classify: {\n DUPLICATE_POSTING: 0.95,\n SIMILAR_ROLE: 0.90,\n RELATED: 0.80,\n },\n filters: ['jobId', 'organizationId'],\n },\n});\n```\n\nUsage:\n```typescript\nconst embeddings = createEmbeddings(ctx.env);\n\n// Different search behaviors for different content types\nconst similarJobs = await embeddings.jobSimilarity.search(text, opts);\nconst similarCandidates = await embeddings.candidateSimilarity.search(text, opts);\nconst matchingApplications = await embeddings.applicationMatch.search(text, opts);\n```\n\n### Table-Level vs Service-Level\n\n| Feature | Table-level (`defineTable`) | Service-level (`defineEmbedding`) |\n|---------|----------------------------|----------------------------------|\n| Auto-embed on INSERT | ✅ | ❌ |\n| Auto-embed on UPDATE | ✅ | ❌ |\n| Custom search functions | ❌ | ✅ |\n| Classification thresholds | ❌ | ✅ |\n| Gray zone detection | ❌ | ✅ |\n| Filterable searches | ❌ | ✅ |\n| Queue message triggers | ❌ | ✅ |\n\n**Use both together:**\n- `defineTable` with `embeddings` config for automatic embedding generation\n- `defineEmbedding` for typed search functions with classification"
379
391
  },
380
392
  "stack/vector": {
381
393
  "title": "Vector & AI",
@@ -383,7 +395,7 @@ export const DOCS = {
383
395
  },
384
396
  "stack/vector/using-embeddings": {
385
397
  "title": "Using Embeddings",
386
- "content": "This page covers the runtime API for working with embeddings — the generated endpoints, the search service, and practical usage patterns.\n\n## Embeddings API\n\nWhen any feature has embeddings configured, the compiler generates these endpoints:\n\n### Generate Embedding\n\n```\nPOST /api/v1/embeddings\n```\n\nEnqueue an embedding job for arbitrary content:\n\n```bash\ncurl -X POST https://api.example.com/api/v1/embeddings \\\n -H \"Content-Type: application/json\" \\\n -H \"Cookie: better-auth.session_token=...\" \\\n -d '{\n \"content\": \"Text to generate an embedding for\",\n \"model\": \"@cf/baai/bge-base-en-v1.5\",\n \"table\": \"claims\",\n \"id\": \"clm_123\"\n }'\n```\n\n**Request body:**\n\n| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n| `content` | string | Yes | Text to embed |\n| `model` | string | No | Embedding model (defaults to table config or `@cf/baai/bge-base-en-v1.5`) |\n| `table` | string | No | Table to store the embedding in |\n| `id` | string | Conditional | Record ID (required when `table` is provided) |\n\n**Response:**\n\n```json\n{\n \"queued\": true,\n \"jobId\": \"550e8400-e29b-41d4-a716-446655440000\",\n \"table\": \"claims\",\n \"id\": \"clm_123\",\n \"model\": \"@cf/baai/bge-base-en-v1.5\"\n}\n```\n\nThis endpoint is useful for:\n- Re-embedding existing records\n- Embedding content that doesn't go through CRUD routes\n- Batch embedding via scripts\n\n### List Embedding Tables\n\n```\nGET /api/v1/embeddings/tables\n```\n\nList tables that have embeddings configured:\n\n```json\n{\n \"tables\": [\n {\n \"name\": \"claims\",\n \"embeddingColumn\": \"embedding\",\n \"model\": \"@cf/baai/bge-base-en-v1.5\"\n }\n ]\n}\n```\n\n## Search Service\n\nFor typed similarity search with classification, use the `createEmbeddings()` service generated from `defineEmbedding()` configurations.\n\n### Basic Search\n\n```typescript\nconst embeddings = createEmbeddings(ctx.env);\n\nconst results = await embeddings.claimSimilarity.search(\n \"Police arrested three suspects\",\n {\n storyId: \"story_123\",\n limit: 5,\n threshold: 0.70,\n }\n);\n\n// Returns: [{ id, score, classification, metadata }]\nfor (const match of results) {\n console.log(`${match.classification}: ${match.id} (score: ${match.score})`);\n}\n```\n\n### Classification\n\nResults are automatically classified based on similarity score:\n\n| Classification | Default Threshold | Meaning |\n|----------------|-------------------|---------|\n| `DUPLICATE` | >= 0.90 | Near-identical content |\n| `CONFIRMS` | >= 0.85 | Strongly supports same claim |\n| `RELATED` | >= 0.75 | Topically related |\n| `NEW` | < 0.75 | No significant match |\n\n### Gray Zone Detection\n\nFor cases where automatic classification isn't sufficient, use gray zone detection:\n\n```typescript\nconst results = await embeddings.claimSimilarity.findWithGrayZone(\n \"Some claim text\",\n { min: 0.60, max: 0.85 }\n);\n\n// results.high_confidence — Score >= 0.85 (auto-classified)\n// results.gray_zone — 0.60 <= score < 0.85 (needs review)\n```\n\n### Raw Embedding\n\nGenerate an embedding vector without searching:\n\n```typescript\nconst vector = await embeddings.claimSimilarity.embed(\"Text to embed\");\n// Returns: number[] (768 dimensions for bge-base)\n```\n\n## Vectorize Queries\n\nIf your project uses a Vectorize index, you can query it directly for custom search logic:\n\n```typescript\n// Generate embedding for query text\nconst queryVector = await embeddings.claimSimilarity.embed(searchText);\n\n// Query Vectorize with metadata filters\nconst results = await ctx.env.VECTORIZE.query(queryVector, {\n topK: 10,\n filter: {\n storyId: \"story_123\",\n organizationId: ctx.activeOrgId,\n },\n});\n```\n\nThe `metadata` fields in your `defineTable` embeddings config are automatically included in the Vectorize index, enabling filtered searches.\n\n## Auto-Embed vs Manual\n\n| Trigger | How | Use Case |\n|---------|-----|----------|\n| Auto (INSERT) | Compiler enqueues after successful create | Default behavior |\n| Auto (UPDATE) | Compiler enqueues when watched fields change | Keeps embeddings fresh |\n| Manual (API) | `POST /api/v1/embeddings` | Re-embedding, batch jobs |\n| Manual (Queue) | `env.EMBEDDINGS_QUEUE.send(...)` | Custom pipelines |\n\n## Security\n\n- The embeddings API requires authentication\n- Jobs are scoped to the user's `activeOrgId`\n- Embedding jobs are only enqueued after all security checks pass (auth, firewall, guards)\n- The queue consumer is an internal process — it trusts pre-validated jobs\n\n## Supported Models\n\n| Model | Dimensions | Notes |\n|-------|------------|-------|\n| `@cf/baai/bge-base-en-v1.5` | 768 | Default, good general-purpose |\n| `@cf/baai/bge-small-en-v1.5` | 384 | Faster, smaller |\n| `@cf/baai/bge-large-en-v1.5` | 1024 | Higher quality |\n\nSee [Workers AI Models](https://developers.cloudflare.com/workers-ai/models/) for the full list.\n\n## Cloudflare Only\n\nEmbeddings require Cloudflare Workers AI and Queues. They are not available with the Bun or Node runtimes.\n\n## See Also\n\n- [Automatic Embeddings](/stack/vector/embeddings) — `defineTable()` config, `defineEmbedding()` search service, and Vectorize integration\n- [Queues](/stack/queues) — How embedding jobs are processed"
398
+ "content": "This page covers the runtime API for working with embeddings — the generated endpoints, the search service, and practical usage patterns.\n\n## Embeddings API\n\nWhen any feature has embeddings configured, the compiler generates these endpoints:\n\n### Generate Embedding\n\n```\nPOST /api/v1/embeddings\n```\n\nEnqueue an embedding job for arbitrary content:\n\n```bash\ncurl -X POST https://api.example.com/api/v1/embeddings \\\n -H \"Content-Type: application/json\" \\\n -H \"Cookie: better-auth.session_token=...\" \\\n -d '{\n \"content\": \"Text to generate an embedding for\",\n \"model\": \"@cf/baai/bge-base-en-v1.5\",\n \"table\": \"jobs\",\n \"id\": \"job_123\"\n }'\n```\n\n**Request body:**\n\n| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n| `content` | string | Yes | Text to embed |\n| `model` | string | No | Embedding model (defaults to table config or `@cf/baai/bge-base-en-v1.5`) |\n| `table` | string | No | Table to store the embedding in |\n| `id` | string | Conditional | Record ID (required when `table` is provided) |\n\n**Response:**\n\n```json\n{\n \"queued\": true,\n \"jobId\": \"550e8400-e29b-41d4-a716-446655440000\",\n \"table\": \"jobs\",\n \"id\": \"job_123\",\n \"model\": \"@cf/baai/bge-base-en-v1.5\"\n}\n```\n\nThis endpoint is useful for:\n- Re-embedding existing records\n- Embedding content that doesn't go through CRUD routes\n- Batch embedding via scripts\n\n### List Embedding Tables\n\n```\nGET /api/v1/embeddings/tables\n```\n\nList tables that have embeddings configured:\n\n```json\n{\n \"tables\": [\n {\n \"name\": \"jobs\",\n \"embeddingColumn\": \"embedding\",\n \"model\": \"@cf/baai/bge-base-en-v1.5\"\n }\n ]\n}\n```\n\n## Search Service\n\nFor typed similarity search with classification, use the `createEmbeddings()` service generated from `defineEmbedding()` configurations.\n\n### Basic Search\n\n```typescript\nconst embeddings = createEmbeddings(ctx.env);\n\nconst results = await embeddings.jobSimilarity.search(\n \"Senior backend engineer with distributed systems experience\",\n {\n department: \"Engineering\",\n limit: 5,\n threshold: 0.70,\n }\n);\n\n// Returns: [{ id, score, classification, metadata }]\nfor (const match of results) {\n console.log(`${match.classification}: ${match.id} (score: ${match.score})`);\n}\n```\n\n### Classification\n\nResults are automatically classified based on similarity score:\n\n| Classification | Default Threshold | Meaning |\n|----------------|-------------------|---------|\n| `DUPLICATE` | >= 0.90 | Near-identical job posting |\n| `SIMILAR_ROLE` | >= 0.85 | Strongly similar role requirements |\n| `RELATED` | >= 0.75 | Topically related position |\n| `NEW` | < 0.75 | No significant match |\n\n### Gray Zone Detection\n\nFor cases where automatic classification isn't sufficient, use gray zone detection:\n\n```typescript\nconst results = await embeddings.jobSimilarity.findWithGrayZone(\n \"Full-stack developer with React and Node.js\",\n { min: 0.60, max: 0.85 }\n);\n\n// results.high_confidence — Score >= 0.85 (auto-classified)\n// results.gray_zone — 0.60 <= score < 0.85 (needs review)\n```\n\n### Raw Embedding\n\nGenerate an embedding vector without searching:\n\n```typescript\nconst vector = await embeddings.jobSimilarity.embed(\"Text to embed\");\n// Returns: number[] (768 dimensions for bge-base)\n```\n\n## Vectorize Queries\n\nIf your project uses a Vectorize index, you can query it directly for custom search logic:\n\n```typescript\n// Generate embedding for query text\nconst queryVector = await embeddings.jobSimilarity.embed(searchText);\n\n// Query Vectorize with metadata filters\nconst results = await ctx.env.VECTORIZE.query(queryVector, {\n topK: 10,\n filter: {\n department: \"Engineering\",\n organizationId: ctx.activeOrgId,\n },\n});\n```\n\nThe `metadata` fields in your `defineTable` embeddings config are automatically included in the Vectorize index, enabling filtered searches.\n\n## Auto-Embed vs Manual\n\n| Trigger | How | Use Case |\n|---------|-----|----------|\n| Auto (INSERT) | Compiler enqueues after successful create | Default behavior |\n| Auto (UPDATE) | Compiler enqueues when watched fields change | Keeps embeddings fresh |\n| Manual (API) | `POST /api/v1/embeddings` | Re-embedding, batch jobs |\n| Manual (Queue) | `env.EMBEDDINGS_QUEUE.send(...)` | Custom pipelines |\n\n## Security\n\n- The embeddings API requires authentication\n- Jobs are scoped to the user's `activeOrgId`\n- Embedding jobs are only enqueued after all security checks pass (auth, firewall, guards)\n- The queue consumer is an internal process — it trusts pre-validated jobs\n\n## Supported Models\n\n| Model | Dimensions | Notes |\n|-------|------------|-------|\n| `@cf/baai/bge-base-en-v1.5` | 768 | Default, good general-purpose |\n| `@cf/baai/bge-small-en-v1.5` | 384 | Faster, smaller |\n| `@cf/baai/bge-large-en-v1.5` | 1024 | Higher quality |\n\nSee [Workers AI Models](https://developers.cloudflare.com/workers-ai/models/) for the full list.\n\n## Cloudflare Only\n\nEmbeddings require Cloudflare Workers AI and Queues. They are not available with the Bun or Node runtimes.\n\n## See Also\n\n- [Automatic Embeddings](/stack/vector/embeddings) — `defineTable()` config, `defineEmbedding()` search service, and Vectorize integration\n- [Queues](/stack/queues) — How embedding jobs are processed"
387
399
  },
388
400
  "stack/webhooks/inbound": {
389
401
  "title": "Inbound Webhooks",
@@ -437,6 +449,7 @@ export const TOPIC_LIST = [
437
449
  "compiler/config/variables",
438
450
  "compiler/definitions/access",
439
451
  "compiler/definitions/actions",
452
+ "compiler/definitions/concepts",
440
453
  "compiler/definitions/firewall",
441
454
  "compiler/definitions/guards",
442
455
  "compiler/definitions",
@@ -448,6 +461,7 @@ export const TOPIC_LIST = [
448
461
  "compiler/getting-started/full-example",
449
462
  "compiler/getting-started/hand-crafted",
450
463
  "compiler/getting-started",
464
+ "compiler/getting-started/patterns",
451
465
  "compiler/getting-started/template-bun",
452
466
  "compiler/getting-started/template-cloudflare",
453
467
  "compiler/getting-started/templates",
@@ -456,6 +470,7 @@ export const TOPIC_LIST = [
456
470
  "compiler/integrations",
457
471
  "compiler/integrations/neon",
458
472
  "compiler/integrations/supabase",
473
+ "compiler/troubleshooting",
459
474
  "compiler/using-the-api/actions-api",
460
475
  "compiler/using-the-api/batch-operations",
461
476
  "compiler/using-the-api/crud",