@kardoe/quickback 0.6.3 → 0.6.4

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.
@@ -208,6 +208,15 @@ export default defineActions(null, {
208
208
 
209
209
  All actions must have `standalone: true` when there is no table.
210
210
 
211
+ ### Public Actions
212
+
213
+ Use `roles: ["PUBLIC"]` for unauthenticated endpoints. Mandatory audit logged. The wildcard `"*"` is not supported.
214
+
215
+ ```typescript
216
+ access: { roles: ["PUBLIC"] } // No auth, audit logged
217
+ access: { roles: ["member"] } // Requires auth + role
218
+ ```
219
+
211
220
  ## Custom Dependencies
212
221
 
213
222
  ```typescript
@@ -1 +1 @@
1
- {"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../../src/docs/content.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,eAAO,MAAM,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAiazC,CAAC;AAEF,eAAO,MAAM,UAAU,UAyGtB,CAAC"}
1
+ {"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../../src/docs/content.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,eAAO,MAAM,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAyazC,CAAC;AAEF,eAAO,MAAM,UAAU,UA2GtB,CAAC"}
@@ -135,7 +135,7 @@ export const DOCS = {
135
135
  },
136
136
  "compiler/config": {
137
137
  "title": "Configuration",
138
- "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| `build` | `object` | Build options (`outputDir`, `packageManager`, `dependencies`) |\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 (enabled by default, `{ generate: false }` to disable) — see [Schema Registry](/cms/schema-registry) |\n\n## Custom Dependencies\n\nAdd third-party npm packages to the generated `package.json` using `build.dependencies`. This is useful when your action handlers import external libraries.\n\n```typescript\nexport default defineConfig({\n name: \"my-app\",\n template: \"hono\",\n build: {\n dependencies: {\n \"fast-xml-parser\": \"^4.5.0\",\n \"lodash-es\": \"^4.17.21\",\n },\n },\n providers: {\n runtime: defineRuntime(\"cloudflare\"),\n database: defineDatabase(\"cloudflare-d1\"),\n auth: defineAuth(\"better-auth\"),\n },\n});\n```\n\nThese are merged into the generated `package.json` alongside Quickback's own dependencies. Run `npm install` after compiling to install them.\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"
138
+ "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: true },\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` | `object` | Feature flags see [Single-Tenant Mode](/compiler/config/single-tenant) |\n| `build` | `object` | Build options (`outputDir`, `packageManager`, `dependencies`) |\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 (enabled by default, `{ generate: false }` to disable) — see [Schema Registry](/cms/schema-registry) |\n| `etag` | `object` | ETag caching for GET responses (enabled by default, `{ enabled: false }` to disable) — see [Caching & ETags](/compiler/using-the-api/caching) |\n\n## Custom Dependencies\n\nAdd third-party npm packages to the generated `package.json` using `build.dependencies`. This is useful when your action handlers import external libraries.\n\n```typescript\nexport default defineConfig({\n name: \"my-app\",\n template: \"hono\",\n build: {\n dependencies: {\n \"fast-xml-parser\": \"^4.5.0\",\n \"lodash-es\": \"^4.17.21\",\n },\n },\n providers: {\n runtime: defineRuntime(\"cloudflare\"),\n database: defineDatabase(\"cloudflare-d1\"),\n auth: defineAuth(\"better-auth\"),\n },\n});\n```\n\nThese are merged into the generated `package.json` alongside Quickback's own dependencies. Run `npm install` after compiling to install them.\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\n- [Single-Tenant Mode](/compiler/config/single-tenant) — Admin-only and public-facing apps without organizations"
139
139
  },
140
140
  "compiler/config/output": {
141
141
  "title": "Output Structure",
@@ -145,17 +145,21 @@ export const DOCS = {
145
145
  "title": "Providers",
146
146
  "content": "Providers configure which services your compiled backend targets.\n\n## Runtime Providers\n\n| Provider | Description |\n|----------|-------------|\n| `cloudflare` | Cloudflare Workers (Hono) |\n| `bun` | Bun runtime (Hono) |\n| `node` | Node.js runtime (Hono) |\n\n```typescript\n\nproviders: {\n runtime: defineRuntime(\"cloudflare\"),\n}\n```\n\n## Database Providers\n\n| Provider | Description |\n|----------|-------------|\n| `cloudflare-d1` | Cloudflare D1 (SQLite) |\n| `better-sqlite3` | SQLite via better-sqlite3 (Bun/Node) |\n| `libsql` | LibSQL / Turso |\n| `neon` | Neon (PostgreSQL) |\n| `supabase` | Supabase (PostgreSQL) |\n\n```typescript\n\nproviders: {\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```\n\n### Database Options\n\n| Option | Type | Default (D1) | Description |\n|--------|------|---------|-------------|\n| `generateId` | `string \\| false` | `\"prefixed\"` | ID generation strategy |\n| `namingConvention` | `string` | `\"snake_case\"` | Column naming convention |\n| `usePlurals` | `boolean` | `false` | Pluralize auth table names (e.g. `user` vs `users`) |\n| `splitDatabases` | `boolean` | `true` | Separate auth and features databases |\n| `authBinding` | `string` | `\"AUTH_DB\"` | Binding name for auth database |\n| `featuresBinding` | `string` | `\"DB\"` | Binding name for features database |\n\n### ID Generation Options\n\n| Value | Description | Example |\n|-------|-------------|---------|\n| `\"uuid\"` | Server generates UUID | `550e8400-e29b-41d4-a716-446655440000` |\n| `\"cuid\"` | Server generates CUID | `clh2v8k9g0000l508h5gx8j1a` |\n| `\"nanoid\"` | Server generates nanoid | `V1StGXR8_Z5jdHi6B-myT` |\n| `\"prefixed\"` | Prefixed ID from table name | `room_abc123` |\n| `\"serial\"` | Database auto-increments | `1`, `2`, `3` |\n| `false` | Client provides ID (enables PUT/upsert) | Any string |\n\n## Auth Providers\n\n| Provider | Description |\n|----------|-------------|\n| `better-auth` | Better Auth with plugins |\n| `supabase-auth` | Supabase Auth |\n| `external` | External auth via Cloudflare service binding |\n\n```typescript\n\nproviders: {\n auth: defineAuth(\"better-auth\", {\n session: {\n expiresInDays: 7,\n updateAgeInDays: 1,\n },\n rateLimit: {\n enabled: true,\n window: 60,\n max: 100,\n },\n }),\n}\n```\n\n### Better Auth Options\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `session.expiresInDays` | `number` | Session expiration in days |\n| `session.updateAgeInDays` | `number` | Session refresh interval in days |\n| `rateLimit.enabled` | `boolean` | Enable rate limiting |\n| `rateLimit.window` | `number` | Rate limit window in seconds |\n| `rateLimit.max` | `number` | Max requests per window |\n| `socialProviders` | `object` | Social login providers (`google`, `github`, `discord`) |\n| `debugLogs` | `boolean` | Enable auth debug logging |\n\n### Better Auth Plugins\n\nWhen `features: [\"organizations\"]` is set in your config, the compiler automatically enables organization-related plugins. Additional plugins can be configured:\n\n| Plugin | Description |\n|--------|-------------|\n| `organization` | Multi-tenant organizations |\n| `admin` | Admin panel access |\n| `apiKey` | API key authentication |\n| `anonymous` | Anonymous sessions |\n| `upgradeAnonymous` | Convert anonymous to full accounts |\n| `twoFactor` | Two-factor authentication |\n| `passkey` | WebAuthn passkey login |\n| `magicLink` | Email magic link login |\n| `emailOtp` | Email one-time password |\n| `deviceAuthorization` | Device auth flow (CLI tools) |\n| `jwt` | JWT token support |\n| `openAPI` | OpenAPI spec generation |\n\n## Storage Providers\n\n```typescript\n\nproviders: {\n storage: defineStorage(\"kv\", {\n binding: \"KV_STORE\",\n }),\n fileStorage: defineFileStorage(\"r2\", {\n binding: \"FILES\",\n maxFileSize: \"10mb\",\n allowedTypes: [\"image/png\", \"image/jpeg\", \"application/pdf\"],\n publicDomain: \"files.example.com\",\n }),\n}\n```\n\n### Storage Types\n\n| Type | Description |\n|------|-------------|\n| `kv` | Key-value storage (Cloudflare KV, Redis) |\n| `r2` | Object storage (Cloudflare R2) |\n| `memory` | In-memory storage (development only) |\n| `redis` | Redis storage |\n\n### File Storage Options\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `binding` | `string` | R2 bucket binding name |\n| `maxFileSize` | `string` | Maximum file size (e.g. `\"10mb\"`) |\n| `allowedTypes` | `string[]` | Allowed MIME types |\n| `publicDomain` | `string` | Public domain for file URLs |\n| `userScopedBuckets` | `boolean` | Scope files by user |"
147
147
  },
148
+ "compiler/config/single-tenant": {
149
+ "title": "Single-Tenant Mode",
150
+ "content": "Single-tenant mode disables the Better Auth organization plugin, removing the `organizationId` requirement from your API. This is ideal for personal sites, blogs, admin panels, or any app with a single tenant.\n\n## Enabling Single-Tenant Mode\n\nSet `features.organizations` to `false` in your config:\n\n```typescript\n\nexport default defineConfig({\n name: \"my-site\",\n template: \"hono\",\n features: {\n organizations: false,\n },\n providers: {\n runtime: defineRuntime(\"cloudflare\"),\n database: defineDatabase(\"cloudflare-d1\"),\n auth: defineAuth(\"better-auth\"),\n },\n});\n```\n\n## What Changes\n\n| Aspect | Multi-Tenant (default) | Single-Tenant |\n|--------|----------------------|---------------|\n| Organization plugin | Enabled | Disabled |\n| `organizationId` columns | Required on resources | Not used |\n| Role source | Org membership table | `user.role` field |\n| Firewall scopes | `owner`, `organization`, `team`, `exception` | `owner`, `exception` only |\n| Admin org endpoints | `/admin/v1/organizations` generated | Skipped |\n| `ctx.activeOrgId` | From session/JWT | Always `undefined` |\n| `ctx.roles` | From org member role | From `user.role` |\n\n## Firewall Configuration\n\nIn single-tenant mode, resources use either **owner** scope (user-scoped data) or **exception** (public/system data):\n\n```typescript\n// User-scoped data (requires authentication)\nexport default defineTable(posts, {\n firewall: { owner: {} },\n crud: {\n list: { access: { roles: ['user', 'admin'] } },\n create: { access: { roles: ['admin'] } },\n },\n});\n\n// Public data (no ownership filtering)\nexport default defineTable(pages, {\n firewall: { exception: true },\n crud: {\n list: { access: { roles: ['PUBLIC'] } },\n get: { access: { roles: ['PUBLIC'] } },\n create: { access: { roles: ['admin'] } },\n update: { access: { roles: ['admin'] } },\n delete: { access: { roles: ['admin'] } },\n },\n});\n```\n\n## Public Routes\n\nUse the `PUBLIC` role to expose routes without authentication. This works the same as in multi-tenant mode:\n\n```typescript\ncrud: {\n list: { access: { roles: ['PUBLIC'] } }, // No auth required\n get: { access: { roles: ['PUBLIC'] } }, // No auth required\n create: { access: { roles: ['admin'] } }, // Admin only\n}\n```\n\n## Role-Based Access\n\nRoles come from the `user.role` field (managed by Better Auth's [admin plugin](https://www.better-auth.com/docs/plugins/admin)):\n\n| Role | Description |\n|------|-------------|\n| `admin` | Full access, set via Better Auth admin plugin |\n| `user` | Default role for authenticated users |\n| `PUBLIC` | Unauthenticated access (special keyword) |\n\n```typescript\n// Admin-only action\nexport default defineActions({\n publish: {\n access: { roles: ['admin'] },\n execute: async ({ ctx, db }) => {\n // ctx.userRole === 'admin'\n // ctx.roles === ['admin']\n },\n },\n});\n```\n\n## Consuming the API\n\nSince single-tenant mode produces a JSON API, pair it with any frontend framework:\n\n```typescript\n// Astro, Next.js, SvelteKit, etc.\nconst posts = await fetch('https://api.mysite.com/api/v1/posts').then(r => r.json());\n\n// Admin operations require authentication\nconst res = await fetch('https://api.mysite.com/api/v1/posts', {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ title: 'New Post', content: '...' }),\n});\n```"
151
+ },
148
152
  "compiler/config/variables": {
149
153
  "title": "Environment Variables",
150
154
  "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"
151
155
  },
152
156
  "compiler/definitions/access": {
153
157
  "title": "Access - Role & Condition-Based Access Control",
154
- "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)"
158
+ "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\n### Special Role: `PUBLIC`\n\nUse `roles: [\"PUBLIC\"]` to make an endpoint accessible without authentication. This is intended for public-facing endpoints like contact forms or webhooks.\n\n```typescript\naccess: { roles: [\"PUBLIC\"] }\n```\n\n**Important:**\n- `PUBLIC` skips authentication, organization, and role checks entirely\n- Every `PUBLIC` action invocation is **mandatory audit logged** to the security audit table (IP address, input, result, timing)\n- The wildcard `\"*\"` is **not supported** — using it will throw a compile-time error\n\n```typescript\n// Contact form — public, no auth\naccess: { roles: [\"PUBLIC\"] }\n\n// Any authenticated user with any org role\naccess: { roles: [\"member\", \"admin\", \"owner\"] }\n\n// Specific roles only\naccess: { roles: [\"admin\"] }\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)"
155
159
  },
156
160
  "compiler/definitions/actions": {
157
161
  "title": "Actions",
158
- "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### Actions-Only Features\n\nYou can create feature directories that contain **only standalone actions** — no table definitions required. This is useful for utility endpoints like reports, integrations, or webhooks that don't map to a specific resource.\n\n```\nquickback/features/\n└── reports/\n ├── actions.ts # Standalone actions only (no table file)\n └── handlers/\n ├── trial-balance.ts\n └── profit-loss.ts\n```\n\n```typescript\n// features/reports/actions.ts\nexport default defineActions(null, {\n trialBalance: {\n description: \"Generate trial balance report\",\n standalone: true,\n path: \"/reports/trial-balance\",\n method: \"GET\",\n input: z.object({\n startDate: z.string(),\n endDate: z.string(),\n }),\n access: { roles: [\"admin\", \"accountant\"] },\n handler: \"./handlers/trial-balance\",\n },\n});\n```\n\nPass `null` as the schema argument since there is no table. All actions in a tableless feature **must** have `standalone: true` — record-based actions require a table to operate on.\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\nThe detector checks for SQL keywords in string arguments, so non-SQL method calls like `headers.get(\"X-Forwarded-For\")` or `map.get(\"key\")` will not trigger false positives.\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```"
162
+ "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### Actions-Only Features\n\nYou can create feature directories that contain **only standalone actions** — no table definitions required. This is useful for utility endpoints like reports, integrations, or webhooks that don't map to a specific resource.\n\n```\nquickback/features/\n└── reports/\n ├── actions.ts # Standalone actions only (no table file)\n └── handlers/\n ├── trial-balance.ts\n └── profit-loss.ts\n```\n\n```typescript\n// features/reports/actions.ts\nexport default defineActions(null, {\n trialBalance: {\n description: \"Generate trial balance report\",\n standalone: true,\n path: \"/reports/trial-balance\",\n method: \"GET\",\n input: z.object({\n startDate: z.string(),\n endDate: z.string(),\n }),\n access: { roles: [\"admin\", \"accountant\"] },\n handler: \"./handlers/trial-balance\",\n },\n});\n```\n\nPass `null` as the schema argument since there is no table. All actions in a tableless feature **must** have `standalone: true` — record-based actions require a table to operate on.\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### Public Access\n\nUse `roles: [\"PUBLIC\"]` for unauthenticated endpoints (contact forms, webhooks, public APIs). Every invocation is **mandatory audit logged** with IP address, input, result, and timing.\n\n```typescript\n// features/contact/actions.ts\nexport default defineActions(null, {\n submit: {\n description: \"Public contact form submission\",\n standalone: true,\n path: \"/submit\",\n input: z.object({\n name: z.string().min(1),\n email: z.string().email(),\n message: z.string().min(10),\n }),\n access: { roles: [\"PUBLIC\"] },\n handler: \"./handlers/submit\",\n },\n});\n```\n\nThe wildcard `\"*\"` is not supported and will throw a compile-time error. Use `\"PUBLIC\"` explicitly.\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\n`PUBLIC` actions also receive mandatory audit logging (same audit table), since unauthenticated endpoints are high-risk by nature.\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\nThe detector checks for SQL keywords in string arguments, so non-SQL method calls like `headers.get(\"X-Forwarded-For\")` or `map.get(\"key\")` will not trigger false positives.\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```"
159
163
  },
160
164
  "compiler/definitions/concepts": {
161
165
  "title": "Glossary",
@@ -253,6 +257,10 @@ export const DOCS = {
253
257
  "title": "Batch Operations",
254
258
  "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"
255
259
  },
260
+ "compiler/using-the-api/caching": {
261
+ "title": "Caching & ETags",
262
+ "content": "Quickback APIs include automatic [ETag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) support on all read endpoints. This lets clients and edge caches avoid re-transferring unchanged data, reducing latency and bandwidth.\n\n## How it works\n\n1. Every GET, LIST, and VIEW response includes a weak ETag header computed from the JSON body:\n ```\n ETag: W/\"a1b2c3d4e5f60718\"\n ```\n\n2. On subsequent requests, the client sends the ETag back:\n ```\n If-None-Match: W/\"a1b2c3d4e5f60718\"\n ```\n\n3. If the data hasn't changed, the server returns `304 Not Modified` with no body — saving bandwidth and parse time.\n\n4. If the data has changed, the server returns the full response with a new ETag.\n\n## Which endpoints support ETags\n\n| Endpoint | ETag | Notes |\n|----------|------|-------|\n| `GET /` (list) | Yes | |\n| `GET /:id` (get) | Yes | |\n| `GET /views/:name` (view) | Yes | |\n| `POST /` (create) | No | Mutations don't cache |\n| `PUT /:id` (update) | No | |\n| `DELETE /:id` (delete) | No | |\n| Actions | No | Side-effectful |\n| Batch operations | No | Mutations |\n\n## Security\n\nETags are computed **after** all security pillars are applied (Firewall, Access, Guards, Masking). This means:\n\n- Different users with different masking rules get different ETags\n- Firewall-scoped data produces ETags specific to that scope\n- There is no risk of leaking data across users via cached ETags\n\n## Response headers\n\nWhen ETags are enabled, GET responses include:\n\n```\nContent-Type: application/json\nETag: W/\"a1b2c3d4e5f60718\"\nCache-Control: no-cache\n```\n\n`Cache-Control: no-cache` means the browser (or edge cache) **must revalidate** on every request, but can use the cached response if the ETag matches. This is the correct behavior for authenticated APIs — data is always fresh, but unchanged responses avoid re-transfer.\n\n## Client usage\n\n### fetch API\n\n```typescript\n// First request\nconst res = await fetch(\"/api/v1/jobs\");\nconst etag = res.headers.get(\"ETag\");\nconst data = await res.json();\n\n// Subsequent request with ETag\nconst res2 = await fetch(\"/api/v1/jobs\", {\n headers: { \"If-None-Match\": etag },\n});\n\nif (res2.status === 304) {\n // Data hasn't changed, use cached version\n} else {\n const freshData = await res2.json();\n}\n```\n\n### Axios / ky / other clients\n\nMost HTTP clients handle `304` responses automatically when configured with an interceptor or cache adapter.\n\n## Configuration\n\nETags are enabled by default. To disable them, set `etag.enabled` to `false` in `quickback.config.ts`:\n\n```typescript\nexport default defineConfig({\n name: \"my-app\",\n // ...\n etag: {\n enabled: false,\n },\n});\n```\n\nWhen disabled, GET responses use standard `c.json()` with no ETag headers or `304` handling.\n\n| Option | Default | Description |\n|--------|---------|-------------|\n| `enabled` | `true` | Include ETag headers on GET/LIST/VIEW responses |\n\n## Technical details\n\n- **Weak ETags** (`W/\"...\"`) — semantically equivalent responses, not byte-identical\n- **SHA-256 truncated to 16 hex chars** — sufficient for collision avoidance, keeps the header small\n- **Web Crypto API** — uses `crypto.subtle.digest()`, available in Cloudflare Workers and Node 18+\n- **No dependencies** — zero additional packages required"
263
+ },
256
264
  "compiler/using-the-api/crud": {
257
265
  "title": "CRUD Endpoints",
258
266
  "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."
@@ -455,6 +463,7 @@ export const TOPIC_LIST = [
455
463
  "compiler/config",
456
464
  "compiler/config/output",
457
465
  "compiler/config/providers",
466
+ "compiler/config/single-tenant",
458
467
  "compiler/config/variables",
459
468
  "compiler/definitions/access",
460
469
  "compiler/definitions/actions",
@@ -482,6 +491,7 @@ export const TOPIC_LIST = [
482
491
  "compiler/troubleshooting",
483
492
  "compiler/using-the-api/actions-api",
484
493
  "compiler/using-the-api/batch-operations",
494
+ "compiler/using-the-api/caching",
485
495
  "compiler/using-the-api/crud",
486
496
  "compiler/using-the-api/errors",
487
497
  "compiler/using-the-api",
@@ -1 +1 @@
1
- {"version":3,"file":"content.js","sourceRoot":"","sources":["../../src/docs/content.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"content.js","sourceRoot":"","sources":["../../src/docs/content.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import pc from "picocolors";
12
12
  // Version injected at build time by scripts/inject-version.ts
13
- const CLI_VERSION = "0.6.3";
13
+ const CLI_VERSION = "0.6.4";
14
14
  function getPackageVersion() {
15
15
  return CLI_VERSION;
16
16
  }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=api-client.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-client.test.d.ts","sourceRoot":"","sources":["../../src/lib/api-client.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,133 @@
1
+ import { afterEach, describe, expect, it, mock } from 'bun:test';
2
+ /**
3
+ * Tests for API client error handling.
4
+ *
5
+ * Ensures that error responses from the compiler API (JSON body with
6
+ * error details, stderr, stdout, etc.) are surfaced in the thrown Error
7
+ * message rather than swallowed.
8
+ */
9
+ // Stub auth so callCompiler doesn't try to read real credentials
10
+ mock.module('./auth.js', () => ({
11
+ getStoredToken: () => Promise.resolve('fake-token'),
12
+ }));
13
+ // We need to control fetch globally
14
+ const originalFetch = globalThis.fetch;
15
+ afterEach(() => {
16
+ globalThis.fetch = originalFetch;
17
+ });
18
+ describe('callCompiler error responses', () => {
19
+ function mockFetch(responses) {
20
+ let call = 0;
21
+ globalThis.fetch = (async () => {
22
+ return responses[call++] ?? responses[responses.length - 1];
23
+ });
24
+ }
25
+ // Health check returns ready, then the compile call returns the given response
26
+ function mockHealthThenCompile(compileResponse) {
27
+ let call = 0;
28
+ globalThis.fetch = (async (url) => {
29
+ if (call === 0 || (typeof url === 'string' && url.includes('/health'))) {
30
+ call++;
31
+ return new Response(JSON.stringify({ status: 'ready', version: '0.5.12' }), {
32
+ status: 200,
33
+ headers: { 'content-type': 'application/json' },
34
+ });
35
+ }
36
+ call++;
37
+ return compileResponse;
38
+ });
39
+ }
40
+ const dummyInput = {
41
+ config: { name: 'test', preset: 'cloudflare', template: 'hono', providers: {} },
42
+ features: [],
43
+ };
44
+ it('includes error message from JSON 500 response', async () => {
45
+ const { callCompiler } = await import('./api-client.js');
46
+ mockHealthThenCompile(new Response(JSON.stringify({
47
+ error: 'Compilation failed',
48
+ message: 'Action "submit" uses roles: ["*"]. Use roles: ["PUBLIC"] instead.',
49
+ }), { status: 500, statusText: 'Internal Server Error', headers: { 'content-type': 'application/json' } }));
50
+ try {
51
+ await callCompiler(dummyInput);
52
+ expect.unreachable('should have thrown');
53
+ }
54
+ catch (err) {
55
+ expect(err.message).toContain('Compilation failed');
56
+ expect(err.message).toContain('500 Internal Server Error');
57
+ // The actual error message from the compiler must be present
58
+ expect(err.message).not.toBe('Compilation failed');
59
+ }
60
+ });
61
+ it('includes details, stderr, and stdout from CompilationError response', async () => {
62
+ const { callCompiler } = await import('./api-client.js');
63
+ mockHealthThenCompile(new Response(JSON.stringify({
64
+ error: 'Compilation failed',
65
+ code: 'CLI_COMMAND_FAILED',
66
+ message: 'Critical command failed: drizzle-kit generate',
67
+ details: {
68
+ command: 'drizzle-kit generate',
69
+ exitCode: 1,
70
+ purpose: 'Generate migration files',
71
+ causes: ['Missing dependency'],
72
+ fixes: ['Install deps'],
73
+ },
74
+ stderr: 'Error: Cannot find module drizzle-orm',
75
+ stdout: 'Running drizzle-kit...',
76
+ }), { status: 500, statusText: 'Internal Server Error', headers: { 'content-type': 'application/json' } }));
77
+ try {
78
+ await callCompiler(dummyInput);
79
+ expect.unreachable('should have thrown');
80
+ }
81
+ catch (err) {
82
+ expect(err.message).toContain('Compilation failed');
83
+ expect(err.message).toContain('drizzle-kit generate');
84
+ expect(err.message).toContain('Cannot find module drizzle-orm');
85
+ expect(err.message).toContain('Running drizzle-kit...');
86
+ }
87
+ });
88
+ it('includes raw text for non-JSON 500 responses', async () => {
89
+ const { callCompiler } = await import('./api-client.js');
90
+ // Non-JSON proxy errors get retried, so provide 3 identical responses
91
+ let calls = 0;
92
+ globalThis.fetch = (async (url) => {
93
+ if (typeof url === 'string' && url.includes('/health')) {
94
+ return new Response(JSON.stringify({ status: 'ready', version: '0.5.12' }), {
95
+ status: 200,
96
+ headers: { 'content-type': 'application/json' },
97
+ });
98
+ }
99
+ calls++;
100
+ return new Response('<html>Bad Gateway</html>', {
101
+ status: 500,
102
+ statusText: 'Internal Server Error',
103
+ headers: { 'content-type': 'text/html' },
104
+ });
105
+ });
106
+ try {
107
+ await callCompiler(dummyInput);
108
+ expect.unreachable('should have thrown');
109
+ }
110
+ catch (err) {
111
+ // After all retries exhausted, the raw HTML should be in the error
112
+ expect(err.message).toContain('Bad Gateway');
113
+ expect(err.message).toContain('Response:');
114
+ }
115
+ });
116
+ it('includes hint for 403 responses', async () => {
117
+ const { callCompiler } = await import('./api-client.js');
118
+ mockHealthThenCompile(new Response(JSON.stringify({
119
+ error: '"pro-template" requires a Pro account',
120
+ hint: 'Run: quickback login',
121
+ upgrade: 'https://quickback.dev/pricing',
122
+ }), { status: 403, statusText: 'Forbidden', headers: { 'content-type': 'application/json' } }));
123
+ try {
124
+ await callCompiler(dummyInput);
125
+ expect.unreachable('should have thrown');
126
+ }
127
+ catch (err) {
128
+ expect(err.message).toContain('requires a Pro account');
129
+ expect(err.message).toContain('Run: quickback login');
130
+ }
131
+ });
132
+ });
133
+ //# sourceMappingURL=api-client.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-client.test.js","sourceRoot":"","sources":["../../src/lib/api-client.test.ts"],"names":[],"mappings":"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"}
@@ -234,6 +234,8 @@ Record condition operators: `equals`, `notEquals`, `in`, `notIn`, `greaterThan`,
234
234
 
235
235
  Context variables: `$ctx.userId`, `$ctx.activeOrgId`, `$ctx.activeTeamId`, `$ctx.roles`, `$ctx.isAnonymous`
236
236
 
237
+ **Special role `PUBLIC`**: Use `roles: ["PUBLIC"]` for unauthenticated access (contact forms, webhooks). Every PUBLIC invocation is mandatory audit logged. The wildcard `"*"` is not supported and will fail at compile time.
238
+
237
239
  ### 3. Guards — Field Protection
238
240
 
239
241
  ```typescript
@@ -204,6 +204,20 @@ export default defineActions(null, {
204
204
  ```
205
205
  All actions must have `standalone: true` when there is no table.
206
206
 
207
+ ### Public actions (no auth required)
208
+ Use `roles: ["PUBLIC"]` for unauthenticated endpoints. Every invocation is mandatory audit logged.
209
+ ```typescript
210
+ submit: {
211
+ standalone: true,
212
+ path: "/submit",
213
+ description: "Public contact form",
214
+ input: z.object({ name: z.string(), email: z.string().email(), message: z.string() }),
215
+ access: { roles: ["PUBLIC"] },
216
+ handler: "./handlers/submit",
217
+ }
218
+ ```
219
+ The wildcard `"*"` is NOT supported and will throw a compile error. Use `"PUBLIC"` explicitly.
220
+
207
221
  ### Custom dependencies in generated package.json
208
222
  ```typescript
209
223
  // quickback.config.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kardoe/quickback",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "CLI for Quickback - one-shot backend generator",
5
5
  "author": "Paul Stenhouse",
6
6
  "license": "MIT",
@@ -208,6 +208,15 @@ export default defineActions(null, {
208
208
 
209
209
  All actions must have `standalone: true` when there is no table.
210
210
 
211
+ ### Public Actions
212
+
213
+ Use `roles: ["PUBLIC"]` for unauthenticated endpoints. Mandatory audit logged. The wildcard `"*"` is not supported.
214
+
215
+ ```typescript
216
+ access: { roles: ["PUBLIC"] } // No auth, audit logged
217
+ access: { roles: ["member"] } // Requires auth + role
218
+ ```
219
+
211
220
  ## Custom Dependencies
212
221
 
213
222
  ```typescript
@@ -234,6 +234,8 @@ Record condition operators: `equals`, `notEquals`, `in`, `notIn`, `greaterThan`,
234
234
 
235
235
  Context variables: `$ctx.userId`, `$ctx.activeOrgId`, `$ctx.activeTeamId`, `$ctx.roles`, `$ctx.isAnonymous`
236
236
 
237
+ **Special role `PUBLIC`**: Use `roles: ["PUBLIC"]` for unauthenticated access (contact forms, webhooks). Every PUBLIC invocation is mandatory audit logged. The wildcard `"*"` is not supported and will fail at compile time.
238
+
237
239
  ### 3. Guards — Field Protection
238
240
 
239
241
  ```typescript
@@ -204,6 +204,20 @@ export default defineActions(null, {
204
204
  ```
205
205
  All actions must have `standalone: true` when there is no table.
206
206
 
207
+ ### Public actions (no auth required)
208
+ Use `roles: ["PUBLIC"]` for unauthenticated endpoints. Every invocation is mandatory audit logged.
209
+ ```typescript
210
+ submit: {
211
+ standalone: true,
212
+ path: "/submit",
213
+ description: "Public contact form",
214
+ input: z.object({ name: z.string(), email: z.string().email(), message: z.string() }),
215
+ access: { roles: ["PUBLIC"] },
216
+ handler: "./handlers/submit",
217
+ }
218
+ ```
219
+ The wildcard `"*"` is NOT supported and will throw a compile error. Use `"PUBLIC"` explicitly.
220
+
207
221
  ### Custom dependencies in generated package.json
208
222
  ```typescript
209
223
  // quickback.config.ts