@kardoe/quickback 0.4.0 → 0.4.2
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.
- package/dist/commands/claude.d.ts +13 -0
- package/dist/commands/claude.d.ts.map +1 -0
- package/dist/commands/claude.js +305 -0
- package/dist/commands/claude.js.map +1 -0
- package/dist/commands/compile.d.ts.map +1 -1
- package/dist/commands/compile.js +43 -1
- package/dist/commands/compile.js.map +1 -1
- package/dist/commands/docs.d.ts +10 -0
- package/dist/commands/docs.d.ts.map +1 -0
- package/dist/commands/docs.js +69 -0
- package/dist/commands/docs.js.map +1 -0
- package/dist/commands/login.js +1 -1
- package/dist/commands/skill.d.ts +13 -0
- package/dist/commands/skill.d.ts.map +1 -0
- package/dist/commands/skill.js +305 -0
- package/dist/commands/skill.js.map +1 -0
- package/dist/docs/content.d.ts +7 -0
- package/dist/docs/content.d.ts.map +1 -0
- package/dist/docs/content.js +92 -0
- package/dist/docs/content.js.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/auth.d.ts +5 -1
- package/dist/lib/auth.d.ts.map +1 -1
- package/dist/lib/auth.js +11 -1
- package/dist/lib/auth.js.map +1 -1
- package/dist/lib/compiler-stubs.d.ts +22 -0
- package/dist/lib/compiler-stubs.d.ts.map +1 -1
- package/dist/lib/compiler-stubs.js +27 -0
- package/dist/lib/compiler-stubs.js.map +1 -1
- package/dist/lib/file-loader.d.ts +55 -0
- package/dist/lib/file-loader.d.ts.map +1 -1
- package/dist/lib/file-loader.js +156 -5
- package/dist/lib/file-loader.js.map +1 -1
- package/dist/lib/markdown-renderer.d.ts +8 -0
- package/dist/lib/markdown-renderer.d.ts.map +1 -0
- package/dist/lib/markdown-renderer.js +151 -0
- package/dist/lib/markdown-renderer.js.map +1 -0
- package/package.json +8 -3
- package/src/skill/SKILL.md +452 -0
- package/src/skill/agents/quickback-specialist/AGENT.md +103 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// AUTO-GENERATED - DO NOT EDIT
|
|
2
|
+
// Generated by scripts/bundle-docs.ts from marketing/docs
|
|
3
|
+
export const DOCS = {
|
|
4
|
+
"config": {
|
|
5
|
+
"title": "Compiler",
|
|
6
|
+
"content": "The Quickback compiler transforms your declarative resource definitions into a complete, production-ready API. It analyzes your TypeScript definitions at build time, generates optimized code, validates your security configuration, and creates database migrations.\n\n## What the Compiler Does\n\nWhen you run `quickback compile`, the compiler:\n\n1. **Reads your definitions** - Analyzes all `defineTable()` configurations in your `definitions/` folder\n2. **Validates security** - Checks that firewall, guards, access, and masking are properly configured\n3. **Generates API routes** - Creates REST endpoints for each resource (GET, POST, PATCH, DELETE, plus batch operations)\n4. **Generates actions** - Creates custom endpoints from your `defineActions()` definitions\n5. **Creates middleware** - Generates authentication, authorization, and data validation logic\n6. **Generates TypeScript types** - Creates type-safe interfaces for your API\n7. **Generates migrations** - Automatically runs `drizzle-kit generate` to create database migration files\n\n**Input:**\n```\nquickback/\n├── quickback.config.ts # Compiler configuration\n└── definitions/\n └── features/\n └── rooms/\n ├── rooms.ts # defineTable(...)\n └── actions.ts # defineActions(...)\n```\n\n**Output:**\n```\nsrc/\n├── routes/\n│ └── rooms.ts # Generated API handlers\n├── middleware/\n│ ├── auth.ts # Authentication logic\n│ └── firewall.ts # Data isolation queries\n└── types/\n └── rooms.ts # TypeScript interfaces\n```\n\n## Basic Usage\n\n### Compile Your Project\n\n```bash\nquickback compile\n```\n\nThis command:\n- Reads your `quickback.config.ts`\n- Analyzes all resource definitions\n- Generates the complete API codebase\n- Reports any configuration errors\n\n### After Compilation\n\nOnce compilation succeeds, you need to:\n\n1. **Apply migrations:**\n \n Run the database migration for your provider. The generated `package.json` includes the appropriate migration script for your setup.\n \n > **Note:** The compiler automatically runs `drizzle-kit generate` during compilation, so you only need to apply the migrations.\n\n2. **Deploy your API:**\n ```bash\n npm run deploy # Cloudflare Workers\n # or\n npm start # Local development\n ```\n\n## The Compilation Process\n\n### 1. Configuration Loading\n\nThe compiler reads your `quickback.config.ts`:\n\n```typescript\nexport default {\n name: 'my-app',\n providers: {\n runtime: { name: 'cloudflare' },\n database: { name: 'cloudflare-d1' },\n auth: { name: 'better-auth' }\n }\n};\n```\n\nThis determines:\n- Which database adapter to use (D1, Supabase, etc.)\n- Which authentication system to integrate\n- Which runtime to target (Cloudflare Workers, Node.js, etc.)\n\n### 2. Resource Discovery\n\nThe compiler scans `definitions/features/` and identifies:\n\n**Resources (with routes):**\n```typescript\n// definitions/features/rooms/rooms.ts\nexport default defineTable(rooms, { ... });\n// → Generates: GET/POST/PATCH/DELETE /api/v1/rooms\n// → Also generates: POST/PATCH/DELETE/PUT /api/v1/rooms/batch (auto-enabled)\n```\n\n**Internal tables (no routes):**\n```typescript\n// definitions/features/rooms/room-bookings.ts\nexport const roomBookings = sqliteTable(...);\n// NO default export → No routes generated\n```\n\n**Actions:**\n```typescript\n// definitions/features/rooms/actions.ts\nexport default defineActions(rooms, {\n book: { ... },\n cancel: { ... }\n});\n// → Generates: POST /api/v1/rooms/:id/book, POST /api/v1/rooms/:id/cancel\n```\n\n### 3. Validation\n\nThe compiler validates your configuration:\n\n#### Firewall Validation\n\n✅ **Valid:**\n```typescript\n// Has organization_id column + organization firewall\nexport const rooms = sqliteTable('rooms', {\n id: text('id').primaryKey(),\n organizationId: text('organization_id').notNull(),\n // ...\n});\n\nexport default defineTable(rooms, {\n firewall: { organization: {} },\n // ...\n});\n```\n\n❌ **Invalid:**\n```typescript\n// Has organization_id but no firewall config\nexport const rooms = sqliteTable('rooms', {\n id: text('id').primaryKey(),\n organizationId: text('organization_id').notNull(),\n // ...\n});\n\nexport default defineTable(rooms, {\n // ✗ Missing firewall!\n crud: { ... }\n});\n// Error: Table has organization_id but no firewall defined\n```\n\n#### Guards Validation\n\n✅ **Valid:**\n```typescript\nguards: {\n createable: ['name', 'capacity'],\n updatable: ['name', 'capacity'],\n protected: ['status'], // Only via actions\n immutable: ['createdBy'] // Never changes\n}\n```\n\n❌ **Invalid:**\n```typescript\nguards: {\n createable: ['name', 'status'],\n updatable: ['name', 'capacity'],\n protected: ['status'] // ✗ Can't be in both createable and protected!\n}\n// Error: Field 'status' appears in both createable and protected\n```\n\n#### Access Validation\n\n✅ **Valid:**\n```typescript\ncrud: {\n list: { access: { roles: ['member', 'admin'] } },\n get: { access: { roles: ['member', 'admin'] } },\n create: { access: { roles: ['admin'] } },\n update: { access: { roles: ['admin'] } },\n delete: { access: { roles: ['admin'] } }\n}\n```\n\n❌ **Invalid:**\n```typescript\ncrud: {\n list: { access: { roles: ['member', 'admin'] } },\n get: { access: { roles: ['member', 'admin'] } },\n create: { access: { roles: ['admin'] } },\n // ✗ Missing update and delete access!\n}\n// Error: CRUD operations defined but missing access configuration\n```\n\n#### Batch Operations (Auto-Enabled)\n\nThe compiler automatically generates batch endpoints when corresponding CRUD operations exist:\n\n**Auto-enabled batch operations:**\n```typescript\ncrud: {\n create: { access: { roles: ['member'] } },\n update: { access: { roles: ['member'] } },\n delete: { access: { roles: ['admin'] } }\n}\n// → Automatically generates:\n// - POST /api/v1/rooms/batch (batch create)\n// - PATCH /api/v1/rooms/batch (batch update)\n// - DELETE /api/v1/rooms/batch (batch delete)\n```\n\n**Batch upsert (strict requirements - same as single PUT):**\n```typescript\ncrud: {\n create: { access: { roles: ['member'] } },\n update: { access: { roles: ['member'] } }\n}\n// AND resource config:\nguards: false // No field restrictions required for PUT\n// AND database config:\ndatabase: {\n generateId: false // User provides IDs\n}\n// → Automatically generates:\n// - PUT /api/v1/rooms/batch (batch upsert)\n```\n\n**Customizing batch operations:**\n```typescript\ncrud: {\n create: { access: { roles: ['member'] } },\n batchCreate: {\n access: { roles: ['admin'] }, // Different access\n maxBatchSize: 50, // Lower limit (default: 100)\n allowAtomic: false // Disable atomic mode\n }\n}\n```\n\n**Disabling batch operations:**\n```typescript\ncrud: {\n create: { access: { roles: ['member'] } },\n batchCreate: false // Explicitly disable\n}\n```\n\n**Batch operation features:**\n- **Partial success mode** (default): Process all records, return successes + errors separately\n- **Atomic mode** (opt-in): All-or-nothing with transaction rollback via `options.atomic: true`\n- **Human-readable errors**: Clear messages with layer identification, error codes, and helpful hints\n- **Full security consistency**: Firewall, access, guards, and masking apply to all batch operations\n- **Performance optimized**: Batch database queries, single timestamp for consistency\n\nSee [CRUD Endpoints - Batch Operations](/crud-endpoints#batch-operations) for API usage details.\n\n### 4. Code Generation\n\nThe compiler generates different files based on your provider:\n\n#### Cloudflare Workers (Hono)\n\n```typescript\n// Generated: src/routes/rooms.ts\n\nconst app = new Hono();\n\napp.get('/rooms', async (c) => {\n const ctx = c.get('ctx');\n\n // Apply firewall (organization isolation)\n let query = db.select().from(rooms);\n query = applyFirewall(query, ctx, 'rooms');\n\n // Check access\n await checkAccess(ctx, 'rooms', 'list');\n\n const results = await query;\n return c.json(results);\n});\n\n// ... more routes\n```\n\n#### Supabase (Next.js)\n\n```typescript\n// Generated: src/app/api/rooms/route.ts\n\nexport async function GET(request: Request) {\n const supabase = createClient();\n\n // Firewall automatically applied via RLS policies\n const { data, error } = await supabase\n .from('rooms')\n .select('*');\n\n if (error) throw error;\n return Response.json(data);\n}\n```\n\n### 5. Type Generation\n\nThe compiler generates TypeScript types for your API:\n\n```typescript\n// Generated: src/types/rooms.ts\n\nexport type Room = typeof rooms.$inferSelect;\nexport type RoomInsert = typeof rooms.$inferInsert;\nexport type RoomUpdate = Partial<RoomInsert>;\n\n// API request/response types\nexport interface ListRoomsResponse {\n data: Room[];\n pagination: {\n page: number;\n pageSize: number;\n total: number;\n };\n}\n\nexport interface CreateRoomRequest {\n name: string;\n capacity: number;\n}\n\nexport interface CreateRoomResponse {\n data: Room;\n}\n```\n\n## Compiler Configuration\n\nYou can configure the compiler in `quickback.config.ts`:\n\n```typescript\nexport default {\n name: 'my-app',\n\n database: {\n generateId: 'uuid', // 'uuid' | 'cuid' | 'nanoid' | 'serial' | false\n namingConvention: 'camelCase', // 'camelCase' | 'snake_case'\n usePlurals: true, // Table names: 'rooms' vs 'room'\n },\n\n compiler: {\n features: {\n auditFields: true, // Auto-manage createdAt/By, modifiedAt/By\n }\n },\n\n providers: {\n runtime: { name: 'cloudflare', config: {} },\n database: { name: 'cloudflare-d1', config: {} },\n auth: { name: 'better-auth', config: {} }\n }\n};\n```\n\n### Compiler Options\n\n| Option | Default | Description |\n|--------|---------|-------------|\n| `compiler.features.auditFields` | `true` | Automatically set `createdAt`, `createdBy`, `modifiedAt`, `modifiedBy` |\n| `database.generateId` | `'uuid'` | ID generation strategy for new records |\n| `database.namingConvention` | `'camelCase'` | Column naming in generated SQL |\n| `database.usePlurals` | `true` | Use plural table names |\n\n## Build-Time vs Runtime\n\nUnderstanding what happens at build time vs runtime:\n\n### Build Time (Compilation)\n\n✅ **The compiler does:**\n- Validates security configuration\n- Checks for schema/firewall mismatches\n- Generates API route handlers\n- Creates TypeScript types\n- Prepares migration setup\n\n❌ **The compiler does NOT:**\n- Connect to your database\n- Execute migrations\n- Start a server\n- Make API requests\n- Validate actual data\n\n### Runtime (API Requests)\n\nWhen your API receives a request:\n\n1. **Authentication** - Verify user identity\n2. **Firewall** - Add WHERE clauses for data isolation\n3. **Access** - Check roles and conditions\n4. **Guards** - Validate field modifications\n5. **Masking** - Redact sensitive fields\n6. **Response** - Return data\n\nThe compiled code handles all of this automatically based on your definitions.\n\n## Common Errors\n\n### Missing Firewall\n\n```\nError: Table 'rooms' has organization_id column but no firewall defined\n```\n\n**Fix:** Add firewall configuration:\n```typescript\nexport default defineTable(rooms, {\n firewall: { organization: {} },\n // ...\n});\n```\n\n### Field in Multiple Guards\n\n```\nError: Field 'status' appears in both createable and protected\n```\n\n**Fix:** A field can only be in one guard category:\n```typescript\nguards: {\n createable: ['name', 'capacity'], // Remove 'status' from here\n protected: ['status'] // Keep it only in protected\n}\n```\n\n### Missing Access Configuration\n\n```\nError: CRUD operation 'create' is enabled but has no access configuration\n```\n\n**Fix:** Define access rules for all CRUD operations:\n```typescript\ncrud: {\n create: { access: { roles: ['member', 'admin'] } },\n // ... other operations\n}\n```\n\n### Protected Field Without Action\n\n```\nWarning: Field 'status' is protected but no actions are defined\n```\n\n**Fix:** Create an action to modify the protected field:\n```typescript\n// definitions/features/rooms/actions.ts\nexport default defineActions(rooms, {\n activate: {\n guard: { status: { from: 'inactive', to: 'active' } },\n execute: async ({ db, record }) => {\n // Custom logic\n }\n }\n});\n```\n\n## Incremental Compilation\n\nThe compiler supports incremental updates:\n\n### First Compilation\n\n```bash\nquickback compile\n# Generates complete codebase from scratch\n```\n\n### Subsequent Compilations\n\n```bash\n# Make changes to your definitions\n# Then recompile\nquickback compile\n\n# Only changed resources are regenerated\n# Database migrations are incremental\n```\n\nThe compiler tracks:\n- Which tables changed\n- Which fields were added/removed/modified\n- Which security rules changed\n\nAnd generates only the necessary updates.\n\n## Output Structure\n\nAfter compilation, your project structure looks like:\n\n```\nmy-app/\n├── definitions/ # Your source of truth\n│ └── features/\n│ └── rooms/\n│ ├── rooms.ts\n│ └── actions.ts\n│\n├── src/ # Generated code\n│ ├── routes/\n│ │ └── rooms.ts # API handlers\n│ ├── middleware/\n│ │ ├── auth.ts # Authentication\n│ │ ├── firewall.ts # Data isolation\n│ │ └── access.ts # Authorization\n│ └── types/\n│ └── rooms.ts # TypeScript types\n│\n├── drizzle/ # Migration setup\n│ ├── features/\n│ │ └── meta/ # Drizzle metadata\n│ └── 0000_*.sql # Generated after drizzle-kit generate\n│\n└── quickback.config.ts # Compiler configuration\n```\n\n## Development Workflow\n\nTypical development flow:\n\n1. **Define resources:**\n ```typescript\n // definitions/features/tasks/tasks.ts\n export default defineTable(tasks, { ... });\n ```\n\n2. **Compile:**\n ```bash\n quickback compile\n ```\n \n The compiler automatically runs `drizzle-kit generate` to create migrations.\n\n3. **Review migrations:**\n ```bash\n cat drizzle/0001_*.sql\n ```\n\n4. **Apply migrations:**\n \n Run the database migration for your provider using the script in your generated `package.json`.\n\n5. **Test locally:**\n ```bash\n npm run dev\n ```\n\n6. **Deploy:**\n ```bash\n npm run deploy\n ```\n\n## Advanced Topics\n\n### Custom Templates\n\nThe compiler supports different templates based on your stack:\n\n```typescript\n// quickback.config.ts\nexport default {\n template: 'hono', // 'hono' | 'nextjs' | 'express'\n // ...\n};\n```\n\nEach template generates code optimized for that framework.\n\n### Multiple Databases\n\nFor split database architectures:\n\n```typescript\n// quickback.config.ts\nexport default {\n providers: {\n database: {\n name: 'cloudflare-d1',\n config: {\n splitDatabases: true,\n authBinding: 'AUTH_DB', // Better Auth tables\n featuresBinding: 'FEATURES_DB' // Your resource tables\n }\n }\n }\n};\n```\n\nThe compiler generates separate migration folders and binding references.\n\n### Provider-Specific Features\n\nDifferent providers get different generated code:\n\n**Cloudflare D1:**\n- Uses `integer` timestamps\n- Generates Hono routes\n- Worker bindings configuration\n\n**PostgreSQL/Supabase:**\n- Uses `timestamp` types\n- Generates RLS policies\n- Supabase client code\n\n**MySQL:**\n- Uses `timestamp` types\n- MySQL-specific syntax\n- Connection pool setup\n\n## Performance\n\nThe compiler is fast:\n\n- **Small project (5 resources):** ~1-2 seconds\n- **Medium project (20 resources):** ~3-5 seconds\n- **Large project (100 resources):** ~10-15 seconds\n\nCompilation time scales linearly with the number of resources.\n\n## Debugging Compilation\n\n### Verbose Output\n\n```bash\nquickback compile --verbose\n```\n\nShows detailed information:\n- Files being read\n- Validation steps\n- Code generation progress\n- File write operations\n\n### Dry Run\n\n```bash\nquickback compile --dry-run\n```\n\nValidates configuration without writing files.\n\n### Check Configuration\n\n```bash\nquickback config\n```\n\nDisplays your resolved configuration.\n\n## Next Steps\n\n- [Database Schema](/database-schema) - Define your tables\n- [Firewall](/firewall) - Configure data isolation\n- [Guards](/guards) - Control field modifications\n- [Actions](/actions) - Add custom business logic\n- [CRUD Endpoints](/crud-endpoints) - Understanding generated APIs"
|
|
7
|
+
},
|
|
8
|
+
"features": {
|
|
9
|
+
"title": "Database Schema",
|
|
10
|
+
"content": "Quickback uses [Drizzle ORM](https://orm.drizzle.team/) to define your database schema. With `defineTable`, you combine your schema definition and security configuration in a single file.\n\n## Defining Tables with defineTable\n\nEach table gets its own file with schema and config together. Use the Drizzle dialect that matches your target database:\n\n| Target Database | Import From | Table Function |\n|-----------------|-------------|----------------|\n| Cloudflare D1, Turso, SQLite | `drizzle-orm/sqlite-core` | `sqliteTable` |\n| Supabase, Neon, PostgreSQL | `drizzle-orm/pg-core` | `pgTable` |\n| PlanetScale, MySQL | `drizzle-orm/mysql-core` | `mysqlTable` |\n\n```typescript\n// definitions/features/rooms/rooms.ts\n// For D1/SQLite targets:\n\nexport const rooms = sqliteTable('rooms', {\n id: text('id').primaryKey(),\n name: text('name').notNull(),\n description: text('description'),\n capacity: integer('capacity').notNull().default(10),\n roomType: text('room_type').notNull(),\n isActive: integer('is_active', { mode: 'boolean' }).notNull().default(true),\n\n // Ownership - for firewall data isolation\n organizationId: text('organization_id').notNull(),\n});\n\nexport default defineTable(rooms, {\n firewall: { organization: {} },\n guards: {\n createable: [\"name\", \"description\", \"capacity\", \"roomType\"],\n updatable: [\"name\", \"description\", \"capacity\"],\n },\n crud: {\n list: { access: { roles: [\"member\", \"admin\"] } },\n get: { access: { roles: [\"member\", \"admin\"] } },\n create: { access: { roles: [\"admin\"] } },\n update: { access: { roles: [\"admin\"] } },\n delete: { access: { roles: [\"admin\"] } },\n },\n});\n\nexport type Room = typeof rooms.$inferSelect;\n```\n\n## Column Types\n\nDrizzle supports all standard SQL column types:\n\n| Type | Drizzle Function | Example |\n|------|------------------|---------|\n| String | `text()`, `varchar()` | `text('name')` |\n| Integer | `integer()`, `bigint()` | `integer('count')` |\n| Boolean | `boolean()` | `boolean('is_active')` |\n| Timestamp | `timestamp()` | `timestamp('created_at')` |\n| JSON | `json()`, `jsonb()` | `jsonb('metadata')` |\n| UUID | `uuid()` | `uuid('id')` |\n| Decimal | `decimal()`, `numeric()` | `decimal('price', { precision: 10, scale: 2 })` |\n\n## Column Modifiers\n\n```typescript\n// Required field\nname: text('name').notNull()\n\n// Default value\nisActive: boolean('is_active').default(true)\n\n// Primary key\nid: text('id').primaryKey()\n\n// Unique constraint\nemail: text('email').unique()\n\n// Default to current timestamp\ncreatedAt: timestamp('created_at').defaultNow()\n```\n\n## File Organization\n\nOrganize your tables by feature. Each feature directory contains table files:\n\n```\ndefinitions/\n└── features/\n ├── rooms/\n │ ├── rooms.ts # Main table + config\n │ ├── room-bookings.ts # Related table + config\n │ └── actions.ts # Custom actions\n ├── users/\n │ ├── users.ts # Table + config\n │ └── user-preferences.ts # Related table\n └── organizations/\n └── organizations.ts\n```\n\n**Key points:**\n- Tables with `export default defineTable(...)` get CRUD routes generated\n- Tables without a default export are internal (no routes, used for joins/relations)\n- Route paths are derived from filenames: `room-bookings.ts` → `/api/v1/room-bookings`\n\n### defineTable vs defineResource\n\nBoth functions are available:\n\n- **`defineTable`** - The standard function for defining tables with CRUD routes\n- **`defineResource`** - Alias for `defineTable`, useful when thinking in terms of REST resources\n\n```typescript\n// These are equivalent:\nexport default defineTable(rooms, { /* config */ });\nexport default defineResource(rooms, { /* config */ });\n```\n\n## Internal Tables (No Routes)\n\nFor junction tables or internal data structures that shouldn't have API routes, simply omit the `defineTable` export:\n\n```typescript\n// definitions/features/rooms/room-amenities.ts\n\n// Junction table - no routes needed\nexport const roomAmenities = sqliteTable('room_amenities', {\n roomId: text('room_id').notNull(),\n amenityId: text('amenity_id').notNull(),\n});\n\n// No default export = no CRUD routes generated\n```\n\n## Audit Fields\n\nQuickback automatically adds and manages these audit fields - you don't need to define them:\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `createdAt` | timestamp | Set when record is created |\n| `createdBy` | text | User ID who created the record |\n| `modifiedAt` | timestamp | Updated on every change |\n| `modifiedBy` | text | User ID who last modified |\n| `deletedAt` | timestamp | Set on soft delete (optional) |\n| `deletedBy` | text | User ID who deleted (optional) |\n\nThe soft delete fields (`deletedAt`, `deletedBy`) are only added if your resource uses soft delete mode.\n\n### Disabling Audit Fields\n\nTo disable automatic audit fields for a resource:\n\n```typescript\n// quickback.config.ts\nexport default defineConfig({\n // ...\n compiler: {\n features: {\n auditFields: false, // Disable for entire project\n }\n }\n});\n```\n\nOr per-table:\n\n```typescript\nexport default defineTable(rooms, {\n auditFields: false, // Disable for this table only\n // ...\n});\n```\n\n### Protected System Fields\n\nThese fields are always protected and cannot be set by clients, even with `guards: false`:\n\n- `id` (when `generateId` is not `false`)\n- `createdAt`, `createdBy`\n- `modifiedAt`, `modifiedBy`\n- `deletedAt`, `deletedBy`\n\n## Ownership Fields\n\nFor the firewall to work, include the appropriate ownership columns:\n\n```typescript\n// For organization-scoped data (most common)\norganizationId: text('organization_id').notNull()\n\n// For user-owned data (personal data)\nownerId: text('owner_id').notNull()\n\n// For team-scoped data\nteamId: text('team_id').notNull()\n```\n\n## Relations (Optional)\n\nDrizzle supports defining relations for type-safe joins:\n\n```typescript\n\nexport const roomsRelations = relations(rooms, ({ one, many }) => ({\n organization: one(organizations, {\n fields: [rooms.organizationId],\n references: [organizations.id],\n }),\n bookings: many(bookings),\n}));\n```\n\n## Validation Rules\n\nDefine validation rules for fields to enforce data integrity:\n\n```typescript\nexport default defineTable(rooms, {\n firewall: { organization: {} },\n guards: {\n createable: [\"name\", \"capacity\", \"roomType\"],\n updatable: [\"name\", \"capacity\"],\n },\n validation: {\n name: { minLength: 1, maxLength: 100 },\n capacity: { min: 1, max: 1000 },\n roomType: { enum: ['meeting', 'conference', 'breakout'] },\n },\n crud: { /* ... */ },\n});\n```\n\n| Validation | Type | Description |\n|------------|------|-------------|\n| `minLength` | number | Minimum string length |\n| `maxLength` | number | Maximum string length |\n| `min` | number | Minimum numeric value |\n| `max` | number | Maximum numeric value |\n| `enum` | string[] | Allowed values |\n| `pattern` | string | Regex pattern to match |\n| `email` | boolean | Must be valid email format |\n| `url` | boolean | Must be valid URL format |\n\n## Database Configuration\n\nConfigure database options in your Quickback config:\n\n```typescript\n// quickback.config.ts\nexport default defineConfig({\n name: 'my-app',\n providers: {\n database: defineDatabase('d1', {\n generateId: 'uuid', // 'uuid' | 'cuid' | 'nanoid' | 'serial' | false\n namingConvention: 'camelCase', // 'camelCase' | 'snake_case'\n usePlurals: true, // Table names: 'users' vs 'user'\n }),\n },\n compiler: {\n features: {\n auditFields: true, // Auto-manage audit timestamps\n }\n }\n});\n```\n\n## Choosing Your Dialect\n\nUse the Drizzle dialect that matches your database provider:\n\n### SQLite (D1, Turso, better-sqlite3)\n\n```typescript\n\nexport const posts = sqliteTable('posts', {\n id: text('id').primaryKey(),\n title: text('title').notNull(),\n metadata: text('metadata', { mode: 'json' }), // JSON stored as text\n isPublished: integer('is_published', { mode: 'boolean' }).default(false),\n organizationId: text('organization_id').notNull(),\n});\n```\n\n### PostgreSQL (Supabase, Neon)\n\n```typescript\n\nexport const posts = pgTable('posts', {\n id: serial('id').primaryKey(),\n title: text('title').notNull(),\n metadata: jsonb('metadata'), // Native JSONB\n isPublished: boolean('is_published').default(false),\n organizationId: text('organization_id').notNull(),\n});\n```\n\n### Key Differences\n\n| Feature | SQLite | PostgreSQL |\n|---------|--------|------------|\n| Boolean | `integer({ mode: 'boolean' })` | `boolean()` |\n| JSON | `text({ mode: 'json' })` | `jsonb()` or `json()` |\n| Auto-increment | `integer().primaryKey()` | `serial()` |\n| UUID | `text()` | `uuid()` |\n\n## Next Steps\n\n- [Configure the firewall](/firewall) for data isolation\n- [Set up access control](/access) for CRUD operations\n- [Define guards](/guards) for field modification rules\n- [Add custom actions](/actions) for business logic"
|
|
11
|
+
},
|
|
12
|
+
"firewall": {
|
|
13
|
+
"title": "Firewall - Data Isolation",
|
|
14
|
+
"content": "The firewall generates WHERE clauses automatically to isolate data by user, organization, or team.\n\n## Basic Usage\n\n```typescript\n// features/rooms/rooms.ts\n\nexport const rooms = sqliteTable('rooms', {\n id: text('id').primaryKey(),\n name: text('name').notNull(),\n organizationId: text('organization_id').notNull(),\n});\n\nexport default defineTable(rooms, {\n firewall: { organization: {} }, // Isolate by organization\n // ... guards, crud\n});\n```\n\n## Configuration Options\n\n```typescript\nfirewall: {\n // User-level ownership (personal data)\n owner?: {\n column?: string; // Default: 'ownerId' or 'owner_id'\n source?: string; // Default: 'ctx.userId'\n mode?: 'required' | 'optional'; // Default: 'required'\n };\n\n // Organization-level ownership\n organization?: {\n column?: string; // Default: 'organizationId' or 'organization_id'\n source?: string; // Default: 'ctx.activeOrgId'\n };\n\n // Team-level ownership\n team?: {\n column?: string; // Default: 'teamId' or 'team_id'\n source?: string; // Default: 'ctx.activeTeamId'\n };\n\n // Soft delete filtering\n softDelete?: {\n column?: string; // Default: 'deletedAt'\n };\n\n // Opt-out for public tables (cannot combine with ownership)\n exception?: boolean;\n}\n```\n\n## Context Sources\n\nThe firewall pulls ownership values from the request context:\n\n| Scope | Default Source | Description |\n|-------|----------------|-------------|\n| `owner` | `ctx.userId` | Current authenticated user's ID |\n| `organization` | `ctx.activeOrgId` | User's active organization ID |\n| `team` | `ctx.activeTeamId` | User's active team ID |\n\nThese context values are populated by the auth middleware from the user's session.\n```\n\n## Auto-Detection\n\nQuickback automatically detects firewall scope based on your column names:\n\n| Column Name | Detected Scope |\n|-------------|----------------|\n| `organizationId` or `organization_id` | `organization` |\n| `ownerId` or `owner_id` | `owner` |\n| `teamId` or `team_id` | `team` |\n| `deletedAt` or `deleted_at` | `softDelete` |\n\nThis means you often don't need to specify column names:\n\n```typescript\n// Quickback detects organizationId column automatically\nfirewall: { organization: {} }\n\n// Quickback detects ownerId column automatically\nfirewall: { owner: {} }\n```\n\n## Common Patterns\n\n```typescript\n// Public/system table - no filtering\nfirewall: { exception: true }\n\n// Organization-scoped data (auto-detected from organizationId column)\nfirewall: { organization: {} }\n\n// Personal user data (auto-detected from ownerId column)\nfirewall: { owner: {} }\n\n// Org data with optional owner filtering\nfirewall: {\n organization: {},\n owner: { mode: 'optional' }\n}\n\n// With soft delete (auto-detected from deletedAt column)\nfirewall: {\n organization: {},\n softDelete: {}\n}\n```\n\n## Rules\n\n- Every resource MUST have at least one ownership scope OR `exception: true`\n- Cannot mix `exception: true` with ownership scopes\n\n## Why Can't You Mix `exception` with Ownership?\n\nThey represent opposite intentions:\n- `exception: true` = \"Generate NO WHERE clauses, data is public/global\"\n- Ownership scopes = \"Generate WHERE clauses to filter data\"\n\nCombining them would be contradictory - you can't both filter and not filter.\n\n## Handling \"Some Public, Some Private\" Data\n\nIf you need records that are sometimes public and sometimes scoped, you have two options:\n\n### Option 1: Two Separate Tables\n\nSplit into two tables - one public, one scoped:\n\n```typescript\n// features/templates/template-library.ts - Public templates\nexport default defineTable(templateLibrary, {\n firewall: { exception: true },\n // ...\n});\n\n// features/templates/user-templates.ts - User's custom templates\nexport default defineTable(userTemplates, {\n firewall: { owner: {} },\n // ...\n});\n```\n\n### Option 2: Use Access Control Instead\n\nKeep ownership scope but make access permissive, then control visibility via `access`:\n\n```typescript\n// features/documents/documents.ts\nexport default defineTable(documents, {\n firewall: {\n organization: {}, // Still scoped to org\n },\n crud: {\n list: {\n // Anyone in the org can list, but they see different things\n // based on a \"visibility\" field you check in your app logic\n access: { roles: [\"member\", \"admin\"] },\n },\n get: {\n // Use record conditions to allow public docs OR owned docs\n access: {\n or: [\n { record: { visibility: { equals: \"public\" } } },\n { record: { ownerId: { equals: \"$ctx.userId\" } } },\n { roles: [\"admin\"] }\n ]\n }\n },\n },\n // ...\n});\n```\n\n### Which to Choose?\n\n| Scenario | Recommendation |\n|----------|----------------|\n| Truly global data (app config, public templates) | `exception: true` in separate resource |\n| \"Public within org\" but still org-isolated | Ownership scope + permissive access rules |\n| User can toggle their own data public/private | Ownership scope + `visibility` field + access conditions |"
|
|
15
|
+
},
|
|
16
|
+
"access": {
|
|
17
|
+
"title": "Access - Role & Condition-Based Access Control",
|
|
18
|
+
"content": "Define who can perform CRUD operations and under what conditions.\n\n## Basic Usage\n\n```typescript\n// features/rooms/rooms.ts\n\nexport const rooms = sqliteTable('rooms', {\n id: text('id').primaryKey(),\n name: text('name').notNull(),\n organizationId: text('organization_id').notNull(),\n});\n\nexport default defineTable(rooms, {\n firewall: { organization: {} },\n guards: { createable: [\"name\"], updatable: [\"name\"] },\n crud: {\n list: { access: { roles: [\"member\", \"admin\"] } },\n get: { access: { roles: [\"member\", \"admin\"] } },\n create: { access: { roles: [\"admin\"] } },\n update: { access: { roles: [\"admin\"] } },\n delete: { access: { roles: [\"admin\"] } },\n },\n});\n```\n\n## Configuration Options\n\n```typescript\ninterface Access {\n // Required roles (OR logic - user needs at least one)\n roles?: string[];\n\n // Record-level conditions\n record?: {\n [field: string]: FieldCondition;\n };\n\n // Combinators\n or?: Access[];\n and?: Access[];\n}\n\n// Field conditions - value can be string | number | boolean\ntype FieldCondition =\n | { equals: value | '$ctx.userId' | '$ctx.activeOrgId' }\n | { notEquals: value }\n | { in: value[] }\n | { notIn: value[] }\n | { lessThan: number }\n | { greaterThan: number }\n | { lessThanOrEqual: number }\n | { greaterThanOrEqual: number };\n```\n\n## CRUD Configuration\n\n```typescript\ncrud: {\n // LIST - GET /resource\n list: {\n access: { roles: [\"member\", \"admin\"] },\n pageSize: 25, // Default page size\n maxPageSize: 100, // Client can't exceed this\n fields: ['id', 'name', 'status'], // Selective field returns (optional)\n },\n\n // GET - GET /resource/:id\n get: {\n access: { roles: [\"member\", \"admin\"] },\n fields: ['id', 'name', 'status', 'details'], // Optional field selection\n },\n\n // CREATE - POST /resource\n create: {\n access: { roles: [\"member\", \"admin\"] },\n defaults: { // Default values for new records\n status: 'pending',\n isActive: true,\n },\n },\n\n // UPDATE - PATCH /resource/:id\n update: {\n access: {\n or: [\n { roles: [\"admin\"] },\n { roles: [\"member\"], record: { status: { equals: \"draft\" } } }\n ]\n },\n },\n\n // DELETE - DELETE /resource/:id\n delete: {\n access: { roles: [\"admin\"] },\n mode: \"soft\", // 'soft' (default) or 'hard'\n },\n\n // PUT - PUT /resource/:id (only when generateId: false + guards: false)\n put: {\n access: { roles: [\"admin\", \"sync-service\"] },\n },\n}\n```\n\n## List Filtering (Query Parameters)\n\nThe LIST endpoint automatically supports filtering via query params:\n\n```\nGET /rooms?status=active # Exact match\nGET /rooms?capacity.gt=10 # Greater than\nGET /rooms?capacity.gte=10 # Greater than or equal\nGET /rooms?capacity.lt=50 # Less than\nGET /rooms?capacity.lte=50 # Less than or equal\nGET /rooms?status.ne=deleted # Not equal\nGET /rooms?name.like=Conference # Pattern match (LIKE %value%)\nGET /rooms?status.in=active,pending,review # IN clause\n```\n\n| Operator | Query Param | SQL Equivalent |\n|----------|-------------|----------------|\n| Equals | `?field=value` | `WHERE field = value` |\n| Not equals | `?field.ne=value` | `WHERE field != value` |\n| Greater than | `?field.gt=value` | `WHERE field > value` |\n| Greater or equal | `?field.gte=value` | `WHERE field >= value` |\n| Less than | `?field.lt=value` | `WHERE field < value` |\n| Less or equal | `?field.lte=value` | `WHERE field <= value` |\n| Pattern match | `?field.like=value` | `WHERE field LIKE '%value%'` |\n| In list | `?field.in=a,b,c` | `WHERE field IN ('a','b','c')` |\n\n## Sorting & Pagination\n\n```\nGET /rooms?sort=createdAt&order=desc # Sort by field\nGET /rooms?limit=25&offset=50 # Pagination\n```\n\n- **Default limit**: 50\n- **Max limit**: 100 (or `maxPageSize` if configured)\n- **Default order**: `asc`\n\n## Delete Modes\n\n```typescript\ndelete: {\n access: { roles: [\"admin\"] },\n mode: \"soft\", // Sets deletedAt/deletedBy, record stays in DB\n}\n\ndelete: {\n access: { roles: [\"admin\"] },\n mode: \"hard\", // Permanent deletion from database\n}\n```\n\n## Context Variables\n\nUse `$ctx.` prefix to reference context values in conditions:\n\n```typescript\n// User can only view their own records\naccess: {\n record: { userId: { equals: \"$ctx.userId\" } }\n}\n\n// Nested path support for complex context objects\naccess: {\n record: { ownerId: { equals: \"$ctx.user.id\" } }\n}\n```\n\n### AppContext Reference\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `$ctx.userId` | `string` | Current authenticated user's ID |\n| `$ctx.activeOrgId` | `string` | User's active organization ID |\n| `$ctx.activeTeamId` | `string \\| null` | User's active team ID (if applicable) |\n| `$ctx.roles` | `string[]` | User's roles in current context |\n| `$ctx.isAnonymous` | `boolean` | Whether user is anonymous |\n| `$ctx.user` | `object` | Full user object from auth provider |\n| `$ctx.user.id` | `string` | User ID (nested path example) |\n| `$ctx.user.email` | `string` | User's email address |\n| `$ctx.{property}` | `any` | Any custom context property |\n\n## Function-Based Access\n\nFor complex access logic that can't be expressed declaratively, use a function:\n\n```typescript\ncrud: {\n update: {\n access: async (ctx, record) => {\n // Custom logic - return true to allow, false to deny\n if (ctx.roles.includes('admin')) return true;\n if (record.ownerId === ctx.userId) return true;\n\n // Check custom business logic\n const membership = await checkTeamMembership(ctx.userId, record.teamId);\n return membership.canEdit;\n }\n }\n}\n```\n\nFunction access receives:\n- `ctx`: The full AppContext object\n- `record`: The record being accessed (for get/update/delete operations)"
|
|
19
|
+
},
|
|
20
|
+
"guards": {
|
|
21
|
+
"title": "Guards - Field Modification Rules",
|
|
22
|
+
"content": "Control which fields can be modified in CREATE vs UPDATE operations.\n\n## Basic Usage\n\n```typescript\n// features/invoices/invoices.ts\n\nexport const invoices = sqliteTable('invoices', {\n id: text('id').primaryKey(),\n name: text('name').notNull(),\n amount: integer('amount').notNull(),\n status: text('status').notNull().default('draft'),\n invoiceNumber: text('invoice_number'),\n organizationId: text('organization_id').notNull(),\n});\n\nexport default defineTable(invoices, {\n firewall: { organization: {} },\n guards: {\n createable: [\"name\", \"amount\", \"invoiceNumber\"],\n updatable: [\"name\"],\n protected: {\n status: [\"approve\", \"reject\"],\n amount: [\"reviseAmount\"],\n },\n immutable: [\"invoiceNumber\"],\n },\n crud: {\n // ...\n },\n});\n```\n\n## Configuration Options\n\n```typescript\nguards: {\n // Fields allowed on CREATE\n createable?: string[];\n\n // Fields allowed on UPDATE/PATCH\n updatable?: string[];\n\n // Fields only modifiable via specific actions\n protected?: Record<string, string[]>;\n\n // Fields set on CREATE, never modified after\n immutable?: string[];\n}\n```\n\n## How It Works\n\n| List | What it controls |\n|------|------------------|\n| `createable` | Fields allowed in create (POST) request body |\n| `updatable` | Fields allowed in update (PATCH) request body |\n| `protected` | Fields blocked from CRUD, only modifiable via named actions |\n| `immutable` | Fields allowed on create, blocked on all updates |\n\n**Combining lists:**\n\n- `createable` + `updatable` - Most fields go in both (can set on create AND modify later)\n- `createable` only - Field is set once, cannot be changed via update\n- `protected` - Don't also list in `createable` or `updatable` (they're mutually exclusive)\n- `immutable` - Don't also list in `updatable` (contradiction)\n\n```typescript\nguards: {\n createable: [\"name\", \"description\", \"category\"],\n updatable: [\"name\", \"description\"],\n // \"category\" is only in createable = set once, can't change via update\n protected: {\n status: [\"approve\", \"reject\"], // NOT in createable/updatable\n },\n immutable: [\"invoiceNumber\"], // NOT in updatable\n}\n```\n\n**If a field is not listed anywhere, it cannot be set by the client.**\n\n## System-Managed Fields (Always Protected)\n\nThese are automatically protected - you cannot override:\n- `createdAt`, `createdBy`\n- `modifiedAt`, `modifiedBy`\n- `deletedAt`, `deletedBy`\n\n## Example\n\n```typescript\nguards: {\n createable: [\"name\", \"description\", \"amount\"],\n updatable: [\"name\", \"description\"],\n protected: {\n status: [\"approve\", \"reject\"], // Only via these actions\n amount: [\"reviseAmount\"],\n },\n immutable: [\"invoiceNumber\"],\n}\n```\n\n## Disabling Guards\n\n```typescript\nguards: false // Only system fields protected\n```\n\nWhen `guards: false` is set, all user-defined fields become writable via CRUD operations. This is useful for:\n- External sync scenarios where you need full field control\n- Batch upsert operations where field restrictions would be limiting\n- Simple tables where field-level protection isn't needed\n\n## PUT/Upsert with External IDs\n\nWhen you disable guards AND use client-provided IDs, you unlock PUT (upsert) operations. This is designed for **syncing data from external systems**.\n\n### Requirements for PUT\n\n1. `generateId: false` in database config (client provides IDs)\n2. `guards: false` in resource definition\n\n### How PUT Works\n\n```\nPUT /resource/:id\n├── Record exists? → UPDATE (replace all fields)\n└── Record missing? → CREATE with provided ID\n```\n\n### Database Config\n\n```typescript\n// quickback.config.ts\nexport default {\n database: {\n generateId: false, // Client provides IDs (enables PUT)\n // Other options: 'uuid' | 'cuid' | 'nanoid' | 'serial'\n }\n};\n```\n\n### Table Definition\n\n```typescript\n// features/external/external-accounts.ts\nexport default defineTable(externalAccounts, {\n firewall: {\n organization: {}, // Still isolated by org\n },\n guards: false, // Disables field restrictions\n crud: {\n put: {\n access: { roles: ['admin', 'sync-service'] }\n }\n },\n});\n```\n\n## What's Still Protected with guards: false\n\nEven with `guards: false`, system-managed fields are ALWAYS protected:\n- `createdAt`, `createdBy` - Set on INSERT only\n- `modifiedAt`, `modifiedBy` - Auto-updated\n- `deletedAt`, `deletedBy` - Set on soft delete\n\n## Ownership Auto-Population\n\nWhen PUT creates a new record, ownership fields are auto-set from context:\n\n```typescript\n// Client sends: PUT /accounts/ext-123 { name: \"Acme\" }\n// Server creates:\n{\n id: \"ext-123\", // Client-provided\n name: \"Acme\", // Client-provided\n organizationId: ctx.activeOrgId, // Auto-set from firewall\n createdAt: now, // Auto-set\n createdBy: ctx.userId, // Auto-set\n modifiedAt: now, // Auto-set\n modifiedBy: ctx.userId, // Auto-set\n}\n```\n\n## Use Cases for PUT/External IDs\n\n| Use Case | Why PUT? |\n|----------|----------|\n| External API sync | External system controls the ID |\n| Webhook handlers | Events come with their own IDs |\n| Data migration | Preserve IDs from source system |\n| Idempotent updates | Safe to retry (no duplicate creates) |\n| Bulk upsert | Create or update in one operation |\n\n## ID Generation Options\n\n| `generateId` | PUT Available? | Notes |\n|--------------|----------------|-------|\n| `'uuid'` | No | Server generates UUID |\n| `'cuid'` | No | Server generates CUID |\n| `'nanoid'` | No | Server generates nanoid |\n| `'serial'` | No | Database auto-increments |\n| `false` | Yes (if guards: false) | Client provides ID |\n\n## Compile-Time Validation\n\nThe Quickback compiler validates your guards configuration and will error if:\n\n1. **Field in both `createable` and `protected`** - A field cannot be both client-writable on create and action-only\n2. **Field in both `updatable` and `protected`** - A field cannot be both client-writable on update and action-only\n3. **Field in both `updatable` and `immutable`** - Contradictory: immutable fields cannot be updated\n4. **Field in `protected` doesn't exist in schema** - Referenced field must exist in the table\n5. **Field in `createable`/`updatable`/`immutable` doesn't exist in schema** - All referenced fields must exist"
|
|
23
|
+
},
|
|
24
|
+
"masking": {
|
|
25
|
+
"title": "Masking - Field Redaction",
|
|
26
|
+
"content": "Hide sensitive data from unauthorized users while showing it to those with permission.\n\n## Automatic Masking (Secure by Default)\n\nQuickback automatically applies masking to columns that match sensitive naming patterns. This ensures a high security posture even if you don't explicitly configure masking.\n\n### Sensitive Keywords & Default Masks\n\n| Pattern | Default Mask | Description |\n| :--- | :--- | :--- |\n| `email` | `email` | p***@e****.com |\n| `phone`, `mobile`, `fax` | `phone` | ***-***-4567 |\n| `ssn`, `socialsecurity` | `ssn` | ***-**-6789 |\n| `creditcard`, `cc` | `creditCard` | ****-****-****-1234 |\n| `password`, `secret`, `token` | `redact` | [REDACTED] |\n\n### Build-Time Alerts\n\nIf the compiler auto-detects a sensitive column that you haven't explicitly configured, it will emit a warning:\n\n```bash\n[Warning] Auto-masking enabled for sensitive column \"users.email\". Explicitly configure masking to silence this warning.\n```\n\nTo silence this warning or change the behavior, simply define the field explicitly in your `masking` configuration. Explicit configurations always take precedence over auto-detected defaults.\n\n### Overriding Defaults\n\nIf you want to show a sensitive field to everyone (disable masking) or use a different rule, define it explicitly in the `resource.masking` block:\n\n```typescript\nexport default defineTable(users, {\n masking: {\n // Show email to everyone (overrides auto-masking)\n email: { type: 'email', show: { roles: ['everyone'] } },\n \n // Silence warning but keep secure (owner-only)\n ssn: { type: 'ssn', show: { or: 'owner' } }\n }\n});\n```\n\n## Basic Usage\n\n```typescript\n// features/employees/employees.ts\n\nexport const employees = sqliteTable('employees', {\n id: text('id').primaryKey(),\n name: text('name').notNull(),\n ssn: text('ssn'),\n salary: integer('salary'),\n email: text('email'),\n organizationId: text('organization_id').notNull(),\n});\n\nexport default defineTable(employees, {\n firewall: { organization: {} },\n guards: { createable: [\"name\", \"ssn\", \"salary\", \"email\"], updatable: [\"name\"] },\n masking: {\n ssn: { type: 'ssn', show: { roles: ['hr', 'admin'] } },\n salary: { type: 'redact', show: { roles: ['hr', 'admin'] } },\n email: { type: 'email', show: { or: 'owner' } },\n },\n crud: {\n // ...\n },\n});\n```\n\n## Built-in Mask Types\n\n| Type | Example Input | Masked Output |\n|------|---------------|---------------|\n| `'email'` | `john@yourdomain.com` | `j***@y***.com` |\n| `'phone'` | `555-123-4567` | `***-***-4567` |\n| `'ssn'` | `123-45-6789` | `***-**-6789` |\n| `'creditCard'` | `4111111111111111` | `************1111` |\n| `'name'` | `John Smith` | `J*** S***` |\n| `'redact'` | `anything` | `[REDACTED]` |\n| `'custom'` | (your logic) | (your output) |\n\n## Configuration\n\n```typescript\nmasking: {\n // Basic masking - everyone sees masked value\n taxId: { type: 'ssn' },\n\n // Show unmasked to specific roles\n salary: {\n type: 'redact',\n show: { roles: ['admin', 'hr'] }\n },\n\n // Show unmasked to owner (createdBy === ctx.userId)\n email: {\n type: 'email',\n show: { or: 'owner' }\n },\n\n // Custom mask function\n apiKey: {\n type: 'custom',\n mask: (value) => value.slice(0, 4) + '...' + value.slice(-4),\n show: { roles: ['admin'] }\n },\n}\n```\n\n## Show Conditions\n\n```typescript\nshow: {\n roles?: string[]; // Unmasked if user has any of these roles\n or?: 'owner'; // Unmasked if user is the record owner\n}\n```\n\nThe `'owner'` condition compares against the owner column configured in your firewall. If you have `firewall: { owner: {} }`, the owner column (default: `ownerId` or `owner_id`) is used to determine ownership. If no owner firewall is configured, it falls back to `createdBy`.\n\n## Complete Example\n\n```typescript\n// features/employees/employees.ts\nexport default defineTable(employees, {\n firewall: { organization: {} },\n guards: { createable: [\"name\", \"ssn\", \"salary\"], updatable: [\"name\"] },\n masking: {\n ssn: { type: 'ssn', show: { roles: ['hr', 'admin'] } },\n salary: { type: 'redact', show: { roles: ['hr', 'admin'] } },\n personalEmail: { type: 'email', show: { or: 'owner' } },\n bankAccount: {\n type: 'custom',\n mask: (val) => '****' + val.slice(-4),\n show: { roles: ['payroll'] }\n },\n },\n crud: {\n list: { access: { roles: [\"member\", \"admin\"] } },\n get: { access: { roles: [\"member\", \"admin\"] } },\n create: { access: { roles: [\"hr\", \"admin\"] } },\n update: { access: { roles: [\"hr\", \"admin\"] } },\n delete: { access: { roles: [\"admin\"] } },\n },\n});\n```"
|
|
27
|
+
},
|
|
28
|
+
"actions": {
|
|
29
|
+
"title": "Actions",
|
|
30
|
+
"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 `guard.record` | Not applicable |\n| Response types | JSON only | JSON, stream, file |\n| Use case | Approve invoice, archive order | AI chat, bulk import, webhooks |\n\n## Defining Actions\n\nActions are defined in a separate `actions.ts` file that references your table:\n\n```typescript\n// features/invoices/actions.ts\n\nexport default defineActions(invoices, {\n approve: {\n description: \"Approve invoice for payment\",\n input: z.object({ notes: z.string().optional() }),\n guard: { roles: [\"admin\", \"finance\"] },\n execute: async ({ db, record, ctx }) => {\n // Business logic\n return record;\n },\n },\n});\n```\n\n### Configuration Options\n\n| Option | Required | Description |\n|--------|----------|-------------|\n| `description` | Yes | Human-readable description of the action |\n| `input` | Yes | Zod schema for request validation |\n| `guard` | 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\n*Either `execute` or `handler` is required, not both.\n\n## Record-Based Actions\n\nRecord-based actions operate on an existing record. The record is automatically loaded and validated before your action executes.\n\n**Route pattern:** `{METHOD} /:id/{actionName}`\n\n```\nPOST /invoices/:id/approve\nGET /orders/:id/status\nDELETE /items/:id/archive\n```\n\n### Runtime Flow\n\n1. **Authentication** - User token is validated\n2. **Record Loading** - The record is fetched by ID\n3. **Firewall Check** - Ensures user can access this record\n4. **Guard Check** - Validates roles and preconditions\n5. **Input Validation** - Request body validated against Zod schema\n6. **Execution** - Your action handler runs\n7. **Response** - Result is returned to client\n\n### Example: Invoice Approval\n\n```typescript\n// features/invoices/actions.ts\n\nexport default defineActions(invoices, {\n approve: {\n description: \"Approve invoice for payment\",\n input: z.object({\n notes: z.string().optional(),\n }),\n guard: {\n roles: [\"admin\", \"finance\"],\n record: { status: { equals: \"pending\" } }, // Precondition\n },\n execute: async ({ db, ctx, record, input }) => {\n const [updated] = await db\n .update(invoices)\n .set({\n status: \"approved\",\n approvedBy: ctx.userId,\n approvedAt: new Date(),\n })\n .where(eq(invoices.id, record.id))\n .returning();\n\n return updated;\n },\n },\n});\n```\n\n### Request Example\n\n```\nPOST /invoices/inv_123/approve\nContent-Type: application/json\n\n{\n \"notes\": \"Approved for Q1 budget\"\n}\n```\n\n### Response Example\n\n```json\n{\n \"data\": {\n \"id\": \"inv_123\",\n \"status\": \"approved\",\n \"approvedBy\": \"user_456\",\n \"approvedAt\": \"2024-01-15T14:30:00Z\"\n }\n}\n```\n\n## Standalone Actions\n\nStandalone actions are independent endpoints that don't require a record context. Use `standalone: true` and optionally specify a custom `path`.\n\n**Route pattern:** Custom `path` or `/{actionName}`\n\n```\nPOST /chat\nGET /reports/summary\nPOST /webhooks/stripe\n```\n\n### Example: AI Chat with Streaming\n\n```typescript\nexport default defineActions(sessions, {\n chat: {\n description: \"Send a message to AI\",\n standalone: true,\n path: \"/chat\",\n method: \"POST\",\n responseType: \"stream\",\n input: z.object({\n message: z.string().min(1).max(2000),\n }),\n guard: {\n roles: [\"member\", \"admin\"],\n },\n handler: \"./handlers/chat\",\n },\n});\n```\n\n### Streaming Response Example\n\nFor actions with `responseType: 'stream'`:\n\n```\nContent-Type: text/event-stream\n\ndata: {\"type\": \"start\"}\ndata: {\"type\": \"chunk\", \"content\": \"Hello\"}\ndata: {\"type\": \"chunk\", \"content\": \"! I'm\"}\ndata: {\"type\": \"chunk\", \"content\": \" here to help.\"}\ndata: {\"type\": \"done\"}\n```\n\n### Example: Report Generation\n\n```typescript\nexport default defineActions(invoices, {\n generateReport: {\n description: \"Generate PDF report\",\n standalone: true,\n path: \"/invoices/report\",\n method: \"GET\",\n responseType: \"file\",\n input: z.object({\n startDate: z.string().datetime(),\n endDate: z.string().datetime(),\n }),\n guard: { roles: [\"admin\", \"finance\"] },\n handler: \"./handlers/generate-report\",\n },\n});\n```\n\n## Guard Configuration\n\nGuards control who can execute an action and under what conditions.\n\n### Role-Based Guards\n\n```typescript\nguard: {\n roles: [\"admin\", \"manager\"] // OR logic: user needs any of these roles\n}\n```\n\n### Record Conditions\n\nFor record-based actions, you can require the record to be in a specific state:\n\n```typescript\nguard: {\n roles: [\"admin\"],\n record: {\n status: { equals: \"pending\" } // Precondition\n }\n}\n```\n\n**Supported operators:**\n\n| Operator | Description |\n|----------|-------------|\n| `equals` | Field must equal value |\n| `notEquals` | Field must not equal value |\n| `in` | Field must be one of the values |\n| `notIn` | Field must not be one of the values |\n\n### Context Substitution\n\nUse `$ctx` to reference the current user's context:\n\n```typescript\nguard: {\n record: {\n ownerId: { equals: \"$ctx.userId\" },\n orgId: { equals: \"$ctx.orgId\" }\n }\n}\n```\n\n### OR/AND Combinations\n\n```typescript\nguard: {\n or: [\n { roles: [\"admin\"] },\n {\n roles: [\"member\"],\n record: { ownerId: { equals: \"$ctx.userId\" } }\n }\n ]\n}\n```\n\n### Function Guards\n\nFor complex logic, use a guard function:\n\n```typescript\nguard: async (ctx, record) => {\n return ctx.roles.includes('admin') || record.ownerId === ctx.userId;\n}\n```\n\n## Handler Files\n\nFor complex actions, separate the logic into handler files.\n\n### When to Use Handler Files\n\n- Complex business logic spanning multiple operations\n- External API integrations\n- File generation or processing\n- Logic reused across multiple actions\n\n### Handler Structure\n\n```typescript\n// handlers/generate-report.ts\n\nexport const execute: ActionExecutor = async ({ db, ctx, input, services }) => {\n const invoices = await db\n .select()\n .from(invoicesTable)\n .where(between(invoicesTable.createdAt, input.startDate, input.endDate));\n\n const pdf = await services.pdf.generate(invoices);\n\n return {\n file: pdf,\n filename: `invoices-${input.startDate}-${input.endDate}.pdf`,\n contentType: 'application/pdf',\n };\n};\n```\n\n### Executor Parameters\n\n```typescript\ninterface ActionExecutorParams {\n db: DrizzleDB; // Database instance\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}\n```\n\n## HTTP API Reference\n\n### Request Format\n\n| Method | Input Source | Use Case |\n|--------|--------------|----------|\n| `GET` | Query parameters | Read-only operations, fetching data |\n| `POST` | JSON body | Default, state-changing operations |\n| `PUT` | JSON body | Full replacement operations |\n| `PATCH` | JSON body | Partial updates |\n| `DELETE` | JSON body | Deletion with optional payload |\n\n```typescript\n// GET action - input comes from query params\ngetStatus: {\n method: \"GET\",\n input: z.object({ format: z.string().optional() }),\n // Called as: GET /invoices/:id/getStatus?format=detailed\n}\n\n// POST action (default) - input comes from JSON body\napprove: {\n // method: \"POST\" is implied\n input: z.object({ notes: z.string().optional() }),\n // Called as: POST /invoices/:id/approve with JSON body\n}\n```\n\n### Response Formats\n\n| Type | Content-Type | Use Case |\n|------|--------------|----------|\n| `json` | `application/json` | Standard API responses (default) |\n| `stream` | `text/event-stream` | Real-time streaming (AI chat, live updates) |\n| `file` | Varies | File downloads (reports, exports) |\n\n### Error Codes\n\n| Status | Description |\n|--------|-------------|\n| `400` | Invalid input / validation error |\n| `401` | Not authenticated |\n| `403` | Guard 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\": {\n \"code\": \"VALIDATION_ERROR\",\n \"message\": \"Invalid request data\",\n \"details\": [\n {\n \"field\": \"amount\",\n \"message\": \"Expected positive number\"\n }\n ]\n }\n}\n```\n\n### Throwing Custom Errors\n\n```typescript\nexecute: async ({ ctx, record, input }) => {\n if (record.balance < input.amount) {\n throw new ActionError('INSUFFICIENT_FUNDS', 'Not enough balance', 400);\n }\n // ... continue\n}\n```\n\n## Protected Fields\n\nActions can modify fields that are protected from regular CRUD operations:\n\n```typescript\n// In resource.ts\nguards: {\n protected: {\n status: [\"approve\", \"reject\"], // Only these actions can modify status\n amount: [\"reviseAmount\"],\n }\n}\n```\n\nThis allows the `approve` action to set `status = \"approved\"` even though the field is protected from regular PATCH requests.\n\n## Examples\n\n### Invoice Approval (Record-Based)\n\n```typescript\nexport default defineActions(invoices, {\n approve: {\n description: \"Approve invoice for payment\",\n input: z.object({ notes: z.string().optional() }),\n guard: {\n roles: [\"admin\", \"finance\"],\n record: { status: { equals: \"pending\" } },\n },\n execute: async ({ db, ctx, record, input }) => {\n const [updated] = await db\n .update(invoices)\n .set({\n status: \"approved\",\n approvedBy: ctx.userId,\n notes: input.notes,\n })\n .where(eq(invoices.id, record.id))\n .returning();\n return updated;\n },\n },\n});\n```\n\n### AI Chat (Standalone with Streaming)\n\n```typescript\nexport default defineActions(sessions, {\n chat: {\n description: \"Send a message to AI assistant\",\n standalone: true,\n path: \"/chat\",\n responseType: \"stream\",\n input: z.object({\n message: z.string().min(1).max(2000),\n }),\n guard: { roles: [\"member\", \"admin\"] },\n handler: \"./handlers/chat\",\n },\n});\n```\n\n### Bulk Import (Standalone, No Record)\n\n```typescript\nexport default defineActions(contacts, {\n bulkImport: {\n description: \"Import contacts from CSV\",\n standalone: true,\n path: \"/contacts/import\",\n input: z.object({\n data: z.array(z.object({\n email: z.string().email(),\n name: z.string(),\n })),\n }),\n guard: { roles: [\"admin\"] },\n execute: async ({ db, input }) => {\n const inserted = await db\n .insert(contacts)\n .values(input.data)\n .returning();\n return { imported: inserted.length };\n },\n },\n});\n```"
|
|
31
|
+
},
|
|
32
|
+
"views": {
|
|
33
|
+
"title": "Views - Column Level Security",
|
|
34
|
+
"content": "Views implement **Column Level Security (CLS)** - controlling which columns users can access. This complements **Row Level Security (RLS)** provided by the [Firewall](/firewall), which controls which rows users can access.\n\n| Security Layer | Controls | Quickback Feature |\n|----------------|----------|-------------------|\n| Row Level Security | Which records | Firewall |\n| Column Level Security | Which fields | Views |\n\nViews provide named field projections with role-based access control. Use views to return different sets of columns to different users without duplicating CRUD endpoints.\n\n## When to Use Views\n\n| Concept | Purpose | Example |\n|---------|---------|---------|\n| CRUD list | Full records | Returns all columns |\n| Masking | Hide values | SSN `***-**-6789` |\n| Views | Exclude columns | Only `id`, `name`, `status` |\n\n- **CRUD list** returns all fields to authorized users\n- **Masking** transforms sensitive values but still includes the column\n- **Views** completely exclude columns from the response\n\n## Basic Usage\n\n```typescript\n// features/customers/customers.ts\n\nexport const customers = sqliteTable('customers', {\n id: text('id').primaryKey(),\n name: text('name').notNull(),\n email: text('email').notNull(),\n phone: text('phone'),\n ssn: text('ssn'),\n internalNotes: text('internal_notes'),\n organizationId: text('organization_id').notNull(),\n});\n\nexport default defineTable(customers, {\n masking: {\n ssn: { type: 'ssn', show: { roles: ['admin'] } },\n },\n\n crud: {\n list: { access: { roles: ['member', 'admin'] } },\n get: { access: { roles: ['member', 'admin'] } },\n },\n\n // Views: Named field projections\n views: {\n summary: {\n fields: ['id', 'name', 'email'],\n access: { roles: ['member', 'admin'] },\n },\n full: {\n fields: ['id', 'name', 'email', 'phone', 'ssn', 'internalNotes'],\n access: { roles: ['admin'] },\n },\n report: {\n fields: ['id', 'name', 'email', 'createdAt'],\n access: { roles: ['finance', 'admin'] },\n },\n },\n});\n```\n\n## Generated Endpoints\n\nViews generate GET endpoints at `/{resource}/views/{viewName}`:\n\n```\nGET /api/v1/customers # CRUD list - all fields\nGET /api/v1/customers/views/summary # View - only summary fields\nGET /api/v1/customers/views/full # View - all fields (admin only)\nGET /api/v1/customers/views/report # View - report fields\n```\n\n## Query Parameters\n\nViews support the same query parameters as the list endpoint:\n\n### Pagination\n\n| Parameter | Description | Default | Max |\n|-----------|-------------|---------|-----|\n| `limit` | Number of records to return | 50 | 100 |\n| `offset` | Number of records to skip | 0 | - |\n\n```bash\n# Get first 10 records\nGET /api/v1/customers/views/summary?limit=10\n\n# Get records 11-20\nGET /api/v1/customers/views/summary?limit=10&offset=10\n```\n\n### Filtering\n\n```bash\n# Filter by exact value\nGET /api/v1/customers/views/summary?status=active\n\n# Filter with operators\nGET /api/v1/customers/views/summary?createdAt.gt=2024-01-01\n\n# Multiple filters (AND logic)\nGET /api/v1/customers/views/summary?status=active&email.like=yourdomain.com\n```\n\n### Sorting\n\n| Parameter | Description | Default |\n|-----------|-------------|---------|\n| `sort` | Field to sort by | `createdAt` |\n| `order` | Sort direction (`asc` or `desc`) | `desc` |\n\n```bash\nGET /api/v1/customers/views/summary?sort=name&order=asc\n```\n\n## Security\n\nAll four security pillars apply to views:\n\n| Pillar | Behavior |\n|--------|----------|\n| **Firewall** | WHERE clause applied (same as list) |\n| **Access** | Per-view access control |\n| **Guards** | N/A (read-only) |\n| **Masking** | Applied to returned fields |\n\n### Firewall\n\nViews automatically apply the same firewall conditions as the list endpoint. Users only see records within their organization scope.\n\n### Access Control\n\nEach view has its own access configuration:\n\n```typescript\nviews: {\n // Public view - available to all members\n summary: {\n fields: ['id', 'name', 'status'],\n access: { roles: ['member', 'admin'] },\n },\n // Restricted view - admin only\n full: {\n fields: ['id', 'name', 'status', 'ssn', 'internalNotes'],\n access: { roles: ['admin'] },\n },\n}\n```\n\n### Masking\n\nMasking rules are applied to the returned fields. If a view includes a masked field like `ssn`, the masking rules still apply:\n\n```typescript\nmasking: {\n ssn: { type: 'ssn', show: { roles: ['admin'] } },\n},\n\nviews: {\n // Even if a member accesses the 'full' view, ssn will be masked\n // because masking rules take precedence\n full: {\n fields: ['id', 'name', 'ssn'],\n access: { roles: ['member', 'admin'] },\n },\n}\n```\n\n### How Views and Masking Work Together\n\nViews and masking are orthogonal concerns:\n- **Views** control field selection (which columns appear)\n- **Masking** controls field transformation (how values appear based on role)\n\nExample configuration:\n\n```typescript\nmasking: {\n ssn: { type: 'ssn', show: { roles: ['admin'] } },\n},\n\nviews: {\n summary: {\n fields: ['id', 'name'], // ssn NOT included\n access: { roles: ['member', 'admin'] },\n },\n full: {\n fields: ['id', 'name', 'ssn'], // ssn included\n access: { roles: ['member', 'admin'] },\n },\n}\n```\n\n| Endpoint | Role | `ssn` in response? | `ssn` value |\n|----------|------|-------------------|-------------|\n| `/views/summary` | member | No | N/A |\n| `/views/summary` | admin | No | N/A |\n| `/views/full` | member | Yes | `***-**-6789` |\n| `/views/full` | admin | Yes | `123-45-6789` |\n\n## Response Format\n\nView responses include metadata about the view:\n\n```json\n{\n \"data\": [\n { \"id\": \"cust_123\", \"name\": \"John Doe\", \"email\": \"john@yourdomain.com\" },\n { \"id\": \"cust_456\", \"name\": \"Jane Smith\", \"email\": \"jane@yourdomain.com\" }\n ],\n \"view\": \"summary\",\n \"fields\": [\"id\", \"name\", \"email\"],\n \"pagination\": {\n \"limit\": 50,\n \"offset\": 0,\n \"count\": 2\n }\n}\n```\n\n## Complete Example\n\n```typescript\n// features/employees/employees.ts\nexport default defineTable(employees, {\n guards: {\n createable: ['name', 'email', 'phone', 'department'],\n updatable: ['name', 'email', 'phone'],\n },\n\n masking: {\n ssn: { type: 'ssn', show: { roles: ['hr', 'admin'] } },\n salary: { type: 'redact', show: { roles: ['hr', 'admin'] } },\n personalEmail: { type: 'email', show: { or: 'owner' } },\n },\n\n crud: {\n list: { access: { roles: ['member', 'admin'] } },\n get: { access: { roles: ['member', 'admin'] } },\n create: { access: { roles: ['hr', 'admin'] } },\n update: { access: { roles: ['hr', 'admin'] } },\n delete: { access: { roles: ['admin'] } },\n },\n\n views: {\n // Directory view - public employee info\n directory: {\n fields: ['id', 'name', 'email', 'department'],\n access: { roles: ['member', 'admin'] },\n },\n // HR view - includes sensitive info\n hr: {\n fields: ['id', 'name', 'email', 'phone', 'ssn', 'salary', 'department', 'startDate'],\n access: { roles: ['hr', 'admin'] },\n },\n // Payroll export\n payroll: {\n fields: ['id', 'name', 'ssn', 'salary', 'bankAccount'],\n access: { roles: ['payroll', 'admin'] },\n },\n },\n});\n```"
|
|
35
|
+
},
|
|
36
|
+
"api": {
|
|
37
|
+
"title": "CRUD Endpoints",
|
|
38
|
+
"content": "Quickback automatically generates RESTful CRUD endpoints for each resource you define. This page covers how to use these endpoints.\n\n## Endpoint Overview\n\nFor a resource named `rooms`, Quickback generates:\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| `GET` | `/rooms` | List all records |\n| `GET` | `/rooms/:id` | Get a single record |\n| `POST` | `/rooms` | Create a new record |\n| `POST` | `/rooms/batch` | Batch create multiple records |\n| `PATCH` | `/rooms/:id` | Update a record |\n| `PATCH` | `/rooms/batch` | Batch update multiple records |\n| `DELETE` | `/rooms/:id` | Delete a record |\n| `DELETE` | `/rooms/batch` | Batch delete multiple records |\n| `PUT` | `/rooms/:id` | Upsert a record (requires config) |\n| `PUT` | `/rooms/batch` | Batch upsert multiple records (requires config) |\n\n## List Records\n\n```\nGET /rooms\n```\n\nReturns a paginated list of records the user has access to.\n\n### Query Parameters\n\n| Parameter | Description | Example |\n|-----------|-------------|---------|\n| `limit` | Number of records to return (default: 50, max: 100) | `?limit=25` |\n| `offset` | Number of records to skip | `?offset=50` |\n| `sort` | Field to sort by | `?sort=createdAt` |\n| `order` | Sort direction: `asc` or `desc` | `?order=desc` |\n\n### Filtering\n\nFilter records using query parameters:\n\n```\nGET /rooms?status=active # Exact match\nGET /rooms?capacity.gt=10 # Greater than\nGET /rooms?capacity.gte=10 # Greater than or equal\nGET /rooms?capacity.lt=50 # Less than\nGET /rooms?capacity.lte=50 # Less than or equal\nGET /rooms?status.ne=deleted # Not equal\nGET /rooms?name.like=Conference # Pattern match (LIKE %value%)\nGET /rooms?status.in=active,pending,review # IN clause\n```\n\n### Response\n\n```json\n{\n \"data\": [\n {\n \"id\": \"room_123\",\n \"name\": \"Conference Room A\",\n \"capacity\": 10,\n \"status\": \"active\",\n \"createdAt\": \"2024-01-15T10:30:00Z\"\n }\n ],\n \"meta\": {\n \"total\": 42,\n \"limit\": 50,\n \"offset\": 0\n }\n}\n```\n\n## Get Single Record\n\n```\nGET /rooms/:id\n```\n\nReturns a single record by ID.\n\n### Response\n\n```json\n{\n \"data\": {\n \"id\": \"room_123\",\n \"name\": \"Conference Room A\",\n \"capacity\": 10,\n \"status\": \"active\",\n \"createdAt\": \"2024-01-15T10:30:00Z\"\n }\n}\n```\n\n### Errors\n\n| Status | Description |\n|--------|-------------|\n| `404` | Record not found or not accessible |\n| `403` | User lacks permission to view this record |\n\n## Create Record\n\n```\nPOST /rooms\nContent-Type: application/json\n\n{\n \"name\": \"Conference Room B\",\n \"capacity\": 8,\n \"roomType\": \"meeting\"\n}\n```\n\nCreates a new record. Only fields listed in `guards.createable` are accepted.\n\n### Response\n\n```json\n{\n \"data\": {\n \"id\": \"room_456\",\n \"name\": \"Conference Room B\",\n \"capacity\": 8,\n \"roomType\": \"meeting\",\n \"createdAt\": \"2024-01-15T11:00:00Z\",\n \"createdBy\": \"user_789\"\n }\n}\n```\n\n### Errors\n\n| Status | Description |\n|--------|-------------|\n| `400` | Invalid field or missing required field |\n| `403` | User lacks permission to create records |\n\n## Update Record\n\n```\nPATCH /rooms/:id\nContent-Type: application/json\n\n{\n \"name\": \"Updated Room Name\",\n \"capacity\": 12\n}\n```\n\nUpdates an existing record. Only fields listed in `guards.updatable` are accepted.\n\n### Response\n\n```json\n{\n \"data\": {\n \"id\": \"room_123\",\n \"name\": \"Updated Room Name\",\n \"capacity\": 12,\n \"modifiedAt\": \"2024-01-15T12:00:00Z\",\n \"modifiedBy\": \"user_789\"\n }\n}\n```\n\n### Errors\n\n| Status | Description |\n|--------|-------------|\n| `400` | Invalid field or field not updatable |\n| `403` | User lacks permission to update this record |\n| `404` | Record not found |\n\n## Delete Record\n\n```\nDELETE /rooms/:id\n```\n\nDeletes a record. Behavior depends on the `delete.mode` configuration.\n\n### Soft Delete (default)\n\nSets `deletedAt` and `deletedBy` fields. Record remains in database but is filtered from queries.\n\n### Hard Delete\n\nPermanently removes the record from the database.\n\n### Response\n\n```json\n{\n \"data\": {\n \"id\": \"room_123\",\n \"deleted\": true\n }\n}\n```\n\n### Errors\n\n| Status | Description |\n|--------|-------------|\n| `403` | User lacks permission to delete this record |\n| `404` | Record not found |\n\n## Upsert Record (PUT)\n\n```\nPUT /rooms/:id\nContent-Type: application/json\n\n{\n \"name\": \"External Room\",\n \"capacity\": 20,\n \"externalId\": \"ext-123\"\n}\n```\n\nCreates or updates a record by ID. Requires special configuration:\n\n1. `generateId: false` in database config\n2. `guards: false` in resource definition\n\n### Behavior\n\n- If record exists: Updates all provided fields\n- If record doesn't exist: Creates with the provided ID\n\n### Use Cases\n\n- Syncing data from external systems\n- Webhook handlers with external IDs\n- Idempotent operations (safe to retry)\n\nSee [Guards documentation](/guards#putupsert-with-external-ids) for setup details.\n\n## Batch Operations\n\nQuickback provides batch endpoints for efficient bulk operations. Batch operations automatically inherit from their corresponding CRUD operations and maintain full security layer consistency.\n\n### Batch Create Records\n\n```\nPOST /rooms/batch\nContent-Type: application/json\n\n{\n \"records\": [\n { \"name\": \"Room A\", \"capacity\": 10 },\n { \"name\": \"Room B\", \"capacity\": 20 },\n { \"name\": \"Room C\", \"capacity\": 15 }\n ],\n \"options\": {\n \"atomic\": false\n }\n}\n```\n\nCreates multiple records in a single request. Each record follows the same validation rules as single create operations.\n\n#### Request Body\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `records` | Array | Array of record objects to create |\n| `options.atomic` | Boolean | If `true`, all records succeed or all fail (default: `false`) |\n\n#### Response (Partial Success - Default)\n\n```json\n{\n \"success\": [\n { \"id\": \"room_1\", \"name\": \"Room A\", \"capacity\": 10 },\n { \"id\": \"room_2\", \"name\": \"Room B\", \"capacity\": 20 }\n ],\n \"errors\": [\n {\n \"index\": 2,\n \"record\": { \"name\": \"Room C\", \"capacity\": 15 },\n \"error\": {\n \"error\": \"Field cannot be set during creation\",\n \"layer\": \"guards\",\n \"code\": \"GUARD_FIELD_NOT_CREATEABLE\",\n \"details\": { \"fields\": [\"status\"] }\n }\n }\n ],\n \"meta\": {\n \"total\": 3,\n \"succeeded\": 2,\n \"failed\": 1,\n \"atomic\": false\n }\n}\n```\n\n#### HTTP Status Codes\n\n- `201` - All records created successfully\n- `207` - Partial success (some records failed)\n- `400` - Atomic mode enabled and one or more records failed\n\n#### Batch Size Limit\n\nDefault: 100 records per request (configurable via `maxBatchSize`)\n\n### Batch Update Records\n\n```\nPATCH /rooms/batch\nContent-Type: application/json\n\n{\n \"records\": [\n { \"id\": \"room_1\", \"capacity\": 12 },\n { \"id\": \"room_2\", \"name\": \"Updated Room B\" },\n { \"id\": \"room_3\", \"capacity\": 25 }\n ],\n \"options\": {\n \"atomic\": false\n }\n}\n```\n\nUpdates multiple records in a single request. All records must include an `id` field.\n\n#### Request Body\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `records` | Array | Array of record objects with `id` and fields to update |\n| `options.atomic` | Boolean | If `true`, all records succeed or all fail (default: `false`) |\n\n#### Response\n\n```json\n{\n \"success\": [\n { \"id\": \"room_1\", \"capacity\": 12, \"modifiedAt\": \"2024-01-15T14:00:00Z\" },\n { \"id\": \"room_2\", \"name\": \"Updated Room B\", \"modifiedAt\": \"2024-01-15T14:00:00Z\" }\n ],\n \"errors\": [\n {\n \"index\": 2,\n \"record\": { \"id\": \"room_3\", \"capacity\": 25 },\n \"error\": {\n \"error\": \"Not found\",\n \"layer\": \"firewall\",\n \"code\": \"NOT_FOUND\",\n \"details\": { \"id\": \"room_3\" }\n }\n }\n ],\n \"meta\": {\n \"total\": 3,\n \"succeeded\": 2,\n \"failed\": 1,\n \"atomic\": false\n }\n}\n```\n\n#### Features\n\n- **Batch fetching**: Single database query for all IDs (with firewall)\n- **Per-record access**: Access checks run with record context\n- **Field validation**: Guards apply to each record individually\n\n### Batch Delete Records\n\n```\nDELETE /rooms/batch\nContent-Type: application/json\n\n{\n \"ids\": [\"room_1\", \"room_2\", \"room_3\"],\n \"options\": {\n \"atomic\": false\n }\n}\n```\n\nDeletes multiple records in a single request. Supports both soft and hard delete modes.\n\n#### Request Body\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `ids` | Array | Array of record IDs to delete |\n| `options.atomic` | Boolean | If `true`, all records succeed or all fail (default: `false`) |\n\n#### Response (Soft Delete)\n\n```json\n{\n \"success\": [\n { \"id\": \"room_1\", \"deletedAt\": \"2024-01-15T15:00:00Z\", \"deletedBy\": \"user_789\" },\n { \"id\": \"room_2\", \"deletedAt\": \"2024-01-15T15:00:00Z\", \"deletedBy\": \"user_789\" }\n ],\n \"errors\": [\n {\n \"index\": 2,\n \"id\": \"room_3\",\n \"error\": {\n \"error\": \"Not found\",\n \"layer\": \"firewall\",\n \"code\": \"NOT_FOUND\",\n \"details\": { \"id\": \"room_3\" }\n }\n }\n ],\n \"meta\": {\n \"total\": 3,\n \"succeeded\": 2,\n \"failed\": 1,\n \"atomic\": false\n }\n}\n```\n\n#### Delete Modes\n\n- **Soft delete** (default): Sets `deletedAt`, `deletedBy`, `modifiedAt`, `modifiedBy` fields\n- **Hard delete**: Permanently removes records from database\n\n### Batch Upsert Records\n\n```\nPUT /rooms/batch\nContent-Type: application/json\n\n{\n \"records\": [\n { \"id\": \"room_1\", \"name\": \"Updated Room A\", \"capacity\": 10 },\n { \"id\": \"new_room\", \"name\": \"New Room\", \"capacity\": 30 }\n ],\n \"options\": {\n \"atomic\": false\n }\n}\n```\n\nCreates or updates multiple records in a single request. Creates if ID doesn't exist, updates if it does.\n\n**Strict Requirements** (same as single PUT):\n1. `generateId: false` in database config (user provides IDs)\n2. `guards: false` in resource definition (no field restrictions)\n3. All records must include an `id` field\n\n**Note**: System-managed fields (`createdAt`, `createdBy`, `modifiedAt`, `modifiedBy`, `deletedAt`, `deletedBy`) are always protected and will be rejected if included in the request, regardless of guards configuration.\n\n#### Request Body\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `records` | Array | Array of record objects with `id` and all fields |\n| `options.atomic` | Boolean | If `true`, all records succeed or all fail (default: `false`) |\n\n#### Response\n\n```json\n{\n \"success\": [\n { \"id\": \"room_1\", \"name\": \"Updated Room A\", \"capacity\": 10, \"modifiedAt\": \"2024-01-15T16:00:00Z\" },\n { \"id\": \"new_room\", \"name\": \"New Room\", \"capacity\": 30, \"createdAt\": \"2024-01-15T16:00:00Z\" }\n ],\n \"errors\": [],\n \"meta\": {\n \"total\": 2,\n \"succeeded\": 2,\n \"failed\": 0,\n \"atomic\": false\n }\n}\n```\n\n#### How It Works\n\n1. Batch existence check with firewall\n2. Split records into CREATE and UPDATE batches\n3. Validate new records with `validateCreate()`\n4. Validate existing records with `validateUpdate()`\n5. Check CREATE access for new records\n6. Check UPDATE access for existing records (per-record)\n7. Execute bulk insert and individual updates\n8. Return combined results\n\n### Batch Operation Features\n\n#### Partial Success Mode (Default)\n\nBy default, batch operations use **partial success** mode:\n- All records are processed independently\n- Failed records go into `errors` array with detailed error information\n- Successful records go into `success` array\n- HTTP status `207 Multi-Status` if any errors, `201`/`200` if all success\n\n```json\n{\n \"success\": [ /* succeeded records */ ],\n \"errors\": [\n {\n \"index\": 2,\n \"record\": { /* original input */ },\n \"error\": {\n \"error\": \"Human-readable message\",\n \"layer\": \"guards\",\n \"code\": \"GUARD_FIELD_NOT_CREATEABLE\",\n \"details\": { \"fields\": [\"status\"] },\n \"hint\": \"These fields are set automatically or must be omitted\"\n }\n }\n ],\n \"meta\": {\n \"total\": 10,\n \"succeeded\": 8,\n \"failed\": 2,\n \"atomic\": false\n }\n}\n```\n\n#### Atomic Mode (Opt-in)\n\nEnable **atomic mode** for all-or-nothing behavior:\n\n```json\n{\n \"records\": [ /* ... */ ],\n \"options\": {\n \"atomic\": true\n }\n}\n```\n\n**Atomic mode behavior**:\n- First error immediately stops processing\n- All changes are rolled back (database transaction)\n- HTTP status `400 Bad Request`\n- Returns single error with failure details\n\n```json\n{\n \"error\": \"Batch operation failed in atomic mode\",\n \"layer\": \"validation\",\n \"code\": \"BATCH_ATOMIC_FAILED\",\n \"details\": {\n \"failedAt\": 2,\n \"reason\": { /* the actual error */ }\n },\n \"hint\": \"Transaction rolled back. Fix the error and retry the entire batch.\"\n}\n```\n\n#### Human-Readable Errors\n\nAll batch operation errors include:\n- **Layer identification**: Which security layer rejected the request\n- **Error code**: Machine-readable code for programmatic handling\n- **Clear message**: Human-readable explanation\n- **Details**: Contextual information (fields, IDs, reasons)\n- **Helpful hints**: Actionable guidance for resolution\n\n#### Performance Optimizations\n\n- **Batch size limits**: Default 100 records (prevents memory exhaustion)\n- **Single firewall query**: `WHERE id IN (...)` instead of N queries\n- **Bulk operations**: Single INSERT for multiple records (CREATE, UPSERT)\n- **O(1) lookups**: Map-based record lookup instead of Array.find()\n\n#### Configuration\n\nBatch operations are **auto-enabled** when corresponding CRUD operations exist:\n\n```typescript\n// Auto-enabled - no configuration needed\ncrud: {\n create: { access: { roles: ['member'] } },\n update: { access: { roles: ['member'] } }\n // batchCreate and batchUpdate automatically available\n}\n\n// Customize batch operations\ncrud: {\n create: { access: { roles: ['member'] } },\n batchCreate: {\n access: { roles: ['admin'] }, // Different access rules\n maxBatchSize: 50, // Lower limit\n allowAtomic: false // Disable atomic mode\n }\n}\n\n// Disable batch operations\ncrud: {\n create: { access: { roles: ['member'] } },\n batchCreate: false // Explicitly disable\n}\n```\n\n#### Security Layer Application\n\nBatch operations maintain **full security layer consistency**:\n\n1. **Firewall**: Auto-apply ownership fields, batch fetch with isolation\n2. **Access**: Operation-level for CREATE, per-record for UPDATE/DELETE\n3. **Guards**: Per-record field validation (same rules as single operations)\n4. **Masking**: Applied to success array (respects user permissions)\n5. **Audit**: Single timestamp for entire batch for consistency\n\n## Authentication\n\nAll endpoints require authentication. Include your auth token in the request header:\n\n```\nAuthorization: Bearer <your-token>\n```\n\nThe user's context (userId, roles, organizationId) is extracted from the token and used to:\n\n1. Apply firewall filters (data isolation)\n2. Check access permissions\n3. Set audit fields (createdBy, modifiedBy)\n\n## Error Responses\n\nAll errors follow a consistent format:\n\n```json\n{\n \"error\": {\n \"code\": \"FORBIDDEN\",\n \"message\": \"You do not have permission to perform this action\"\n }\n}\n```\n\n### Common Error Codes\n\n| Code | Status | Description |\n|------|--------|-------------|\n| `BAD_REQUEST` | 400 | Invalid request data |\n| `UNAUTHORIZED` | 401 | Missing or invalid auth token |\n| `FORBIDDEN` | 403 | Insufficient permissions |\n| `NOT_FOUND` | 404 | Record not found |\n| `VALIDATION_ERROR` | 400 | Field validation failed |"
|
|
39
|
+
},
|
|
40
|
+
"quickstart": {
|
|
41
|
+
"title": "Quick Start",
|
|
42
|
+
"content": "Get started with Quickback in minutes. This guide shows you how to define a complete table with security configuration.\n\n## File Structure\n\nEach table gets its own file with schema and config together using `defineTable`:\n\n```\ndefinitions/\n└── features/\n └── rooms/\n ├── rooms.ts # Table + security config\n ├── room-bookings.ts # Related table + config\n ├── actions.ts # Custom actions (optional)\n └── handlers/ # Action handlers (optional)\n └── activate.ts\n```\n\n## Complete Example\n\nHere's a complete `rooms` table with all security layers:\n\n```typescript\n// definitions/features/rooms/rooms.ts\n\nexport const rooms = sqliteTable('rooms', {\n id: text('id').primaryKey(),\n name: text('name').notNull(),\n description: text('description'),\n capacity: integer('capacity').notNull().default(10),\n roomType: text('room_type').notNull(),\n isActive: integer('is_active', { mode: 'boolean' }).notNull().default(true),\n deactivationReason: text('deactivation_reason'),\n\n // Ownership - required for firewall data isolation\n organizationId: text('organization_id').notNull(),\n});\n\nexport default defineTable(rooms, {\n // 1. FIREWALL - Data isolation\n firewall: { organization: {} },\n\n // 2. GUARDS - Field modification rules\n guards: {\n createable: [\"name\", \"description\", \"capacity\", \"roomType\"],\n updatable: [\"name\", \"description\", \"capacity\"],\n protected: {\n isActive: [\"activate\", \"deactivate\"], // Only via actions\n },\n },\n\n // 3. CRUD - Role-based access control\n crud: {\n list: { access: { roles: [\"member\", \"admin\"] }, pageSize: 25 },\n get: { access: { roles: [\"member\", \"admin\"] } },\n create: { access: { roles: [\"admin\"] } },\n update: { access: { roles: [\"admin\"] } },\n delete: { access: { roles: [\"admin\"] }, mode: \"soft\" },\n },\n});\n\nexport type Room = typeof rooms.$inferSelect;\n```\n\n## Adding Custom Actions\n\nActions let you add business logic beyond CRUD:\n\n```typescript\n// definitions/features/rooms/actions.ts\n\nexport default defineActions(rooms, {\n activate: {\n description: \"Activate a deactivated room\",\n input: z.object({}),\n guard: {\n roles: [\"admin\"],\n record: { isActive: { equals: false } },\n },\n execute: async ({ db, record }) => {\n const [updated] = await db\n .update(rooms)\n .set({ isActive: true })\n .where(eq(rooms.id, record.id))\n .returning();\n return updated;\n },\n },\n\n deactivate: {\n description: \"Deactivate a room\",\n input: z.object({\n reason: z.string().optional(),\n }),\n guard: {\n roles: [\"admin\"],\n record: { isActive: { equals: true } },\n },\n execute: async ({ db, record, input }) => {\n const [updated] = await db\n .update(rooms)\n .set({ isActive: false, deactivationReason: input.reason })\n .where(eq(rooms.id, record.id))\n .returning();\n return updated;\n },\n },\n});\n```\n\n## Generated API Endpoints\n\nQuickback generates these endpoints from the example above:\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| `GET` | `/api/v1/rooms` | List rooms (members, admins) |\n| `GET` | `/api/v1/rooms/:id` | Get single room |\n| `POST` | `/api/v1/rooms` | Create room (admins only) |\n| `PATCH` | `/api/v1/rooms/:id` | Update room (admins only) |\n| `DELETE` | `/api/v1/rooms/:id` | Soft delete room (admins only) |\n| `POST` | `/api/v1/rooms/:id/activate` | Custom action |\n| `POST` | `/api/v1/rooms/:id/deactivate` | Custom action |\n\n## What Each Layer Does\n\n1. **Firewall**: Automatically adds `WHERE organizationId = ?` to every query. Users in Org A can never see Org B's data.\n\n2. **Guards**: Controls which fields can be modified:\n - `createable`: Fields allowed in POST requests\n - `updatable`: Fields allowed in PATCH requests\n - `protected`: Fields that can only be changed via specific actions\n\n3. **CRUD Access**: Role-based access control for each operation. Members can read, only admins can write.\n\n4. **Actions**: Custom business logic with their own guards and validation.\n\n## Internal Tables (No Routes)\n\nTables without `export default defineTable(...)` don't get API routes:\n\n```typescript\n// definitions/features/rooms/room-amenities.ts\n\n// Junction table - used internally, no API routes\nexport const roomAmenities = sqliteTable('room_amenities', {\n roomId: text('room_id').notNull(),\n amenityId: text('amenity_id').notNull(),\n});\n\n// No default export = internal table\n```\n\n## Next Steps\n\nDive deeper into each topic:\n\n- [Database Schema](/database-schema) - Column types, relations, audit fields\n- [Firewall](/firewall) - Data isolation patterns\n- [Access](/access) - Role & condition-based control\n- [Guards](/guards) - Field modification rules\n- [Masking](/masking) - Field redaction for sensitive data\n- [Actions](/actions) - Custom business logic endpoints"
|
|
43
|
+
},
|
|
44
|
+
"concepts": {
|
|
45
|
+
"title": "How It All Fits Together",
|
|
46
|
+
"content": "Before diving into specific features, let's understand how Quickback's pieces connect. This page gives you the mental model for everything that follows.\n\n## The Big Picture\n\nQuickback is a **backend compiler**. You write definition files, and Quickback compiles them into a production-ready API.\n\n1. **You write definitions** - Table files with schema and security config using `defineTable`\n2. **Quickback compiles them** - Analyzes your definitions at build time\n3. **You get a production API** - `GET /rooms`, `POST /rooms`, `PATCH /rooms/:id`, `DELETE /rooms/:id`, batch operations, plus custom actions\n\n## File Structure\n\nYour definitions live in a `definitions/` folder organized by feature. The preferred structure uses a `quickback/` folder:\n\n```\nmy-app/\n├── quickback/\n│ ├── quickback.config.ts # Compiler configuration\n│ └── definitions/\n│ └── features/\n│ └── {feature-name}/\n│ ├── claims.ts # Table + config (defineTable)\n│ ├── claim-versions.ts # Secondary table + config\n│ ├── claim-sources.ts # Internal table (no routes)\n│ ├── actions.ts # Custom actions (optional)\n│ └── handlers/ # Action handlers (optional)\n│ └── my-action.ts\n├── src/ # Generated code (output)\n├── drizzle/ # Generated migrations\n└── package.json\n```\n\n**Table files** use `defineTable` to combine schema and security config:\n\n```typescript\n// features/claims/claims.ts\n\nexport const claims = sqliteTable(\"claims\", {\n id: text(\"id\").primaryKey(),\n organizationId: text(\"organization_id\").notNull(),\n content: text(\"content\").notNull(),\n});\n\nexport default defineTable(claims, {\n firewall: { organization: {} },\n guards: {\n createable: [\"content\"],\n updatable: [\"content\"],\n },\n crud: {\n list: { access: { roles: [\"member\", \"admin\"] } },\n get: { access: { roles: [\"member\", \"admin\"] } },\n create: { access: { roles: [\"member\", \"admin\"] } },\n update: { access: { roles: [\"member\", \"admin\"] } },\n delete: { access: { roles: [\"admin\"] } },\n },\n});\n\nexport type Claim = typeof claims.$inferSelect;\n```\n\n**Key points:**\n- Tables with `export default defineTable(...)` → CRUD routes generated\n- Tables without default export → internal/junction tables (no routes)\n- Route path derived from filename: `claim-versions.ts` → `/api/v1/claim-versions`\n\n## The Four Security Layers\n\nEvery API request passes through four security layers, in order:\n\n```\nRequest → Firewall → Access → Guards → Masking → Response\n │ │ │ │\n │ │ │ └── Hide sensitive fields\n │ │ └── Block field modifications\n │ └── Check roles & conditions\n └── Isolate data by owner/org/team\n```\n\n### 1. Firewall (Data Isolation)\n\nThe firewall controls **which records** a user can see. It automatically adds WHERE clauses to every query based on your schema columns:\n\n| Column in Schema | What happens |\n|------------------|--------------|\n| `organization_id` | Data isolated by organization |\n| `user_id` | Data isolated by user (personal data) |\n\nNo manual configuration needed - Quickback applies smart rules based on your schema. If a table has `organization_id`, users in Org A can never see Org B's data, even if they guess the record ID.\n\nYou only need to define a firewall when:\n- You need an `exception` for public/global data\n- Both `organization_id` and `user_id` exist and you need to clarify which takes priority\n\n[Learn more about Firewall →](/firewall)\n\n### 2. Access (CRUD Permissions)\n\nAccess controls **which operations** a user can perform. It checks roles and record conditions.\n\n```typescript\ncrud: {\n list: { access: { roles: [\"member\", \"admin\"] } },\n get: { access: { roles: [\"member\", \"admin\"] } },\n create: { access: { roles: [\"admin\"] } },\n update: { access: { roles: [\"admin\"] } },\n delete: { access: { roles: [\"admin\"] } },\n}\n```\n\nYou can also add conditions like \"only update your own records\" or \"only delete drafts\".\n\n[Learn more about Access →](/access)\n\n### 3. Guards (Field Modification Rules)\n\nGuards control **which fields** can be modified in each operation.\n\n| Guard Type | What it means |\n|------------|---------------|\n| `createable` | Fields that can be set when creating |\n| `updatable` | Fields that can be changed when updating |\n| `protected` | Fields that can only be changed via specific actions |\n| `immutable` | Fields that can never be changed after creation |\n\n**Example:** A `status` field might be protected - users can't directly PATCH it, but they can call an `approve` action that changes it.\n\n[Learn more about Guards →](/guards)\n\n### 4. Masking (Data Redaction)\n\nMasking hides sensitive fields from users who shouldn't see them.\n\n```typescript\nmasking: {\n ssn: { mask: 'ssn' }, // Shows: ***-**-1234\n email: { mask: 'email' }, // Shows: j***@y***.com\n salary: { mask: 'redact' }, // Shows: [REDACTED]\n}\n```\n\nYou can conditionally show the real value based on roles or ownership.\n\n[Learn more about Masking →](/masking)\n\n## How They Work Together\n\nHere's a real example of how a request flows through all four layers:\n\n**Scenario:** A member requests `GET /employees/123`\n\n1. **Firewall** checks: Is employee 123 in the user's organization?\n - ✅ Yes → Continue\n - ❌ No → 404 Not Found (as if it doesn't exist)\n\n2. **Access** checks: Can members perform GET?\n - ✅ Yes → Continue\n - ❌ No → 403 Forbidden\n\n3. **Guards** don't apply to GET (they're for writes)\n\n4. **Masking** applies: User is a member, not admin\n - SSN: `123-45-6789` → `***-**-6789`\n - Salary: `85000` → `[REDACTED]`\n - Email: `john@company.com` → `j***@company.com`\n\n5. **Response** sent with masked data\n\n## Locked Down by Default\n\nQuickback is **secure by default**. Nothing is accessible until you explicitly allow it.\n\n| Layer | Default | What you must do |\n|-------|---------|------------------|\n| Firewall | AUTO | Auto-detects from `organization_id`/`user_id` columns. Only configure for exceptions. |\n| Access | DENIED | Explicitly define `access` rules with roles |\n| Guards | LOCKED | Explicitly list `createable`, `updatable` fields |\n| Actions | BLOCKED | Explicitly define `guard` for each action |\n\nThis means:\n- A resource with no definition = completely inaccessible\n- Forgot to add a field to `createable`? = 400 error on create\n- No `access` rule? = 403 forbidden\n\n**You must deliberately open each door.** This prevents accidental data exposure.\n\n## Optional Enhancements\n\nBeyond the core security layers, Quickback supports additional features depending on your deployment stack:\n\n| Feature | Description | Stack |\n|---------|-------------|-------|\n| [Realtime](/cloudflare/realtime) | Live updates via WebSocket with role-based filtering and masking | Cloudflare |\n| [Automatic Embeddings](/cloudflare/embeddings) | AI-powered vector embeddings via Cloudflare Queues | Cloudflare |\n\nThese features integrate with your security configuration - Realtime broadcasts respect your masking rules, and embedding jobs are only enqueued after passing all security checks.\n\n## Next Steps\n\nNow that you understand the mental model, dive into the specifics:\n\n1. [Compiler](/compiler) - Understand how definitions become a working API\n2. [Database Schema](/database-schema) - Define your tables\n3. [Firewall](/firewall) - Set up data isolation\n4. [Access](/access) - Configure CRUD permissions\n5. [Guards](/guards) - Control field modifications\n6. [Masking](/masking) - Hide sensitive data\n7. [Actions](/actions) - Add custom business logic"
|
|
47
|
+
},
|
|
48
|
+
"examples": {
|
|
49
|
+
"title": "Complete Example",
|
|
50
|
+
"content": "This example shows a complete resource definition using all five security layers, and the production code Quickback generates from it.\n\n## What You Define\n\nA `customers` resource for a multi-tenant SaaS application with:\n- Organization-level data isolation\n- Role-based CRUD permissions\n- Field-level write protection\n- PII masking for sensitive fields\n- Column-level views for different access levels\n\n### Schema\n\n```typescript title=\"definitions/features/customers/schema.ts\"\n\nexport const customers = sqliteTable('customers', {\n id: text('id').primaryKey(),\n organizationId: text('organization_id').notNull(),\n name: text('name').notNull(),\n email: text('email').notNull(),\n phone: text('phone'),\n ssn: text('ssn'),\n});\n\n// Note: createdAt, createdBy, modifiedAt, modifiedBy are auto-injected\n```\n\n### Resource Configuration\n\n```typescript title=\"definitions/features/customers/resource.ts\"\n\nexport default defineResource(customers, {\n // FIREWALL: Data isolation\n firewall: {\n organization: {}\n },\n\n // ACCESS: Role-based permissions\n crud: {\n list: { access: { roles: ['member', 'admin', 'support'] } },\n get: { access: { roles: ['member', 'admin', 'support'] } },\n create: { access: { roles: ['admin'] } },\n update: { access: { roles: ['admin', 'support'] } },\n delete: { access: { roles: ['admin'] }, mode: 'hard' },\n },\n\n // GUARDS: Field-level protection\n guards: {\n createable: ['name', 'email', 'phone', 'ssn'],\n updatable: ['name', 'email', 'phone'],\n immutable: ['ssn'], // Can set once, never change\n },\n\n // MASKING: PII redaction\n masking: {\n ssn: { type: 'ssn', show: { roles: ['admin'] } },\n phone: { type: 'phone', show: { roles: ['admin', 'support'] } },\n email: { type: 'email', show: { roles: ['admin'] } },\n },\n\n // VIEWS: Column-level projections\n views: {\n summary: {\n fields: ['id', 'name', 'email'],\n access: { roles: ['member', 'admin', 'support'] },\n },\n full: {\n fields: ['id', 'name', 'email', 'phone', 'ssn'],\n access: { roles: ['admin'] },\n },\n },\n});\n```\n\n---\n\n## What Quickback Generates\n\nRun `quickback compile` and Quickback generates production-ready code.\n\n### Security Helpers\n\n```typescript title=\"src/features/customers/customers.resource.ts\"\n\n/**\n * Firewall conditions for customers\n * Pattern: Organization only\n */\nexport function buildFirewallConditions(ctx: AppContext) {\n const conditions = [];\n\n // Organization isolation\n conditions.push(eq(customers.organizationId, ctx.activeOrgId!));\n\n return and(...conditions);\n}\n\n/**\n * Guards configuration for field modification rules\n */\nexport const GUARDS_CONFIG = {\n createable: new Set(['name', 'email', 'phone', 'ssn']),\n updatable: new Set(['name', 'email', 'phone']),\n immutable: new Set(['ssn']),\n protected: {},\n systemManaged: new Set([\n 'createdAt', 'createdBy',\n 'modifiedAt', 'modifiedBy',\n 'deletedAt', 'deletedBy'\n ]),\n};\n\n/**\n * Validates field input for CREATE operations.\n */\nexport function validateCreate(input: Record<string, any>) {\n const systemManaged: string[] = [];\n const protectedFields: Array<{ field: string; actions: string[] }> = [];\n const notCreateable: string[] = [];\n\n for (const field of Object.keys(input)) {\n if (GUARDS_CONFIG.systemManaged.has(field)) {\n systemManaged.push(field);\n continue;\n }\n if (field in GUARDS_CONFIG.protected) {\n const actions = [...GUARDS_CONFIG.protected[field]];\n protectedFields.push({ field, actions });\n continue;\n }\n if (!GUARDS_CONFIG.createable.has(field) && !GUARDS_CONFIG.immutable.has(field)) {\n notCreateable.push(field);\n }\n }\n\n const valid = systemManaged.length === 0 &&\n protectedFields.length === 0 &&\n notCreateable.length === 0;\n\n return { valid, systemManaged, protected: protectedFields, notCreateable };\n}\n\n/**\n * Validates field input for UPDATE operations.\n */\nexport function validateUpdate(input: Record<string, any>) {\n const systemManaged: string[] = [];\n const immutable: string[] = [];\n const protectedFields: Array<{ field: string; actions: string[] }> = [];\n const notUpdatable: string[] = [];\n\n for (const field of Object.keys(input)) {\n if (GUARDS_CONFIG.systemManaged.has(field)) {\n systemManaged.push(field);\n continue;\n }\n if (GUARDS_CONFIG.immutable.has(field)) {\n immutable.push(field);\n continue;\n }\n if (field in GUARDS_CONFIG.protected) {\n const actions = [...GUARDS_CONFIG.protected[field]];\n protectedFields.push({ field, actions });\n continue;\n }\n if (!GUARDS_CONFIG.updatable.has(field)) {\n notUpdatable.push(field);\n }\n }\n\n const valid = systemManaged.length === 0 &&\n immutable.length === 0 &&\n protectedFields.length === 0 &&\n notUpdatable.length === 0;\n\n return { valid, systemManaged, immutable, protected: protectedFields, notUpdatable };\n}\n\n/**\n * Masks sensitive fields based on user role\n */\nexport function maskCustomer<T extends Record<string, any>>(\n record: T,\n ctx: AppContext\n): T {\n const masked: any = { ...record };\n\n // email: show to admin only\n if (!ctx.roles?.includes('admin')) {\n if (masked['email'] != null) {\n masked['email'] = masks.email(masked['email']);\n }\n }\n\n // phone: show to admin, support\n if (!ctx.roles?.some(r => ['admin', 'support'].includes(r))) {\n if (masked['phone'] != null) {\n masked['phone'] = masks.phone(masked['phone']);\n }\n }\n\n // ssn: show to admin only\n if (!ctx.roles?.includes('admin')) {\n if (masked['ssn'] != null) {\n masked['ssn'] = masks.ssn(masked['ssn']);\n }\n }\n\n return masked as T;\n}\n\nexport function maskCustomers<T extends Record<string, any>>(\n records: T[],\n ctx: AppContext\n): T[] {\n return records.map(r => maskCustomer(r, ctx));\n}\n\n// CRUD Access configuration\nexport const CRUD_ACCESS = {\n list: { access: { roles: ['member', 'admin', 'support'] } },\n get: { access: { roles: ['member', 'admin', 'support'] } },\n create: { access: { roles: ['admin'] } },\n update: { access: { roles: ['admin', 'support'] } },\n delete: { access: { roles: ['admin'] }, mode: 'hard' },\n};\n\n// Views configuration\nexport const VIEWS_CONFIG = {\n summary: {\n fields: ['id', 'name', 'email'],\n access: { roles: ['member', 'admin', 'support'] },\n },\n full: {\n fields: ['id', 'name', 'email', 'phone', 'ssn'],\n access: { roles: ['admin'] },\n },\n};\n```\n\n### API Routes\n\nThe generated routes wire everything together:\n\n```typescript title=\"src/features/customers/customers.routes.ts\"\n\n buildFirewallConditions,\n validateCreate,\n validateUpdate,\n maskCustomer,\n maskCustomers,\n CRUD_ACCESS\n} from './customers.resource';\n\nconst app = new Hono();\n\n// GET /customers - List with firewall + masking\napp.get('/', async (c) => {\n const ctx = c.get('ctx');\n const db = c.get('db');\n\n // Access check\n if (!await evaluateAccess(CRUD_ACCESS.list.access, ctx)) {\n return c.json(AccessErrors.roleRequired(\n CRUD_ACCESS.list.access.roles,\n ctx.roles\n ), 403);\n }\n\n // Query with firewall conditions\n const results = await db.select().from(customers)\n .where(buildFirewallConditions(ctx));\n\n // Apply masking before returning\n return c.json({\n data: maskCustomers(results, ctx),\n });\n});\n\n// POST /customers - Create with guards\napp.post('/', async (c) => {\n const ctx = c.get('ctx');\n const db = c.get('db');\n\n // Access check\n if (!await evaluateAccess(CRUD_ACCESS.create.access, ctx)) {\n return c.json(AccessErrors.roleRequired(\n CRUD_ACCESS.create.access.roles,\n ctx.roles\n ), 403);\n }\n\n const body = await c.req.json();\n\n // Guards validation\n const validation = validateCreate(body);\n if (!validation.valid) {\n if (validation.systemManaged.length > 0) {\n return c.json(GuardErrors.systemManaged(validation.systemManaged), 400);\n }\n if (validation.notCreateable.length > 0) {\n return c.json(GuardErrors.fieldNotCreateable(validation.notCreateable), 400);\n }\n }\n\n // Apply ownership and audit fields\n const data = {\n id: 'cst_' + crypto.randomUUID().replace(/-/g, ''),\n ...body,\n organizationId: ctx.activeOrgId,\n createdAt: new Date().toISOString(),\n createdBy: ctx.userId,\n modifiedAt: new Date().toISOString(),\n modifiedBy: ctx.userId,\n };\n\n const result = await db.insert(customers).values(data).returning();\n return c.json(maskCustomer(result[0], ctx), 201);\n});\n\n// PATCH /customers/:id - Update with guards\napp.patch('/:id', async (c) => {\n const ctx = c.get('ctx');\n const db = c.get('db');\n const id = c.req.param('id');\n\n // Fetch with firewall\n const [record] = await db.select().from(customers)\n .where(and(buildFirewallConditions(ctx), eq(customers.id, id)));\n\n if (!record) {\n return c.json({ error: 'Not found', code: 'NOT_FOUND' }, 404);\n }\n\n // Access check\n if (!await evaluateAccess(CRUD_ACCESS.update.access, ctx)) {\n return c.json(AccessErrors.roleRequired(\n CRUD_ACCESS.update.access.roles,\n ctx.roles\n ), 403);\n }\n\n const body = await c.req.json();\n\n // Guards validation\n const validation = validateUpdate(body);\n if (!validation.valid) {\n if (validation.immutable.length > 0) {\n return c.json(GuardErrors.fieldImmutable(validation.immutable), 400);\n }\n if (validation.notUpdatable.length > 0) {\n return c.json(GuardErrors.fieldNotUpdatable(validation.notUpdatable), 400);\n }\n }\n\n // Apply audit fields\n const data = {\n ...body,\n modifiedAt: new Date().toISOString(),\n modifiedBy: ctx.userId,\n };\n\n const result = await db.update(customers).set(data)\n .where(eq(customers.id, id)).returning();\n\n return c.json(maskCustomer(result[0], ctx));\n});\n\nexport default app;\n```\n\n---\n\n## Runtime Behavior\n\n### Masking by Role\n\n| Role | `email` | `phone` | `ssn` |\n|------|---------|---------|-------|\n| **admin** | `john@example.com` | `555-123-4567` | `123-45-6789` |\n| **support** | `j***@e***.com` | `555-123-4567` | `***-**-6789` |\n| **member** | `j***@e***.com` | `***-***-4567` | `***-**-6789` |\n\n### Views + Masking Interaction\n\nViews control **which fields are returned**. Masking controls **what values are shown**.\n\n| Endpoint | Role | `ssn` in response? | `ssn` value |\n|----------|------|--------------------|-------------|\n| `/customers/views/summary` | member | No | N/A |\n| `/customers/views/summary` | admin | No | N/A |\n| `/customers/views/full` | member | Yes | `***-**-6789` |\n| `/customers/views/full` | admin | Yes | `123-45-6789` |\n\n### Guards Enforcement\n\n```bash\n# ✅ Allowed: Create with createable fields\nPOST /customers\n{ \"name\": \"Jane\", \"email\": \"jane@example.com\", \"ssn\": \"987-65-4321\" }\n\n# ❌ Rejected: Update immutable field\nPATCH /customers/cst_123\n{ \"ssn\": \"000-00-0000\" }\n# → 400: \"Field 'ssn' is immutable and cannot be modified\"\n\n# ❌ Rejected: Set system-managed field\nPOST /customers\n{ \"name\": \"Jane\", \"createdBy\": \"hacker\" }\n# → 400: \"System-managed fields cannot be set: createdBy\"\n```\n\n---\n\n## Generated API Endpoints\n\n| Method | Endpoint | Access |\n|--------|----------|--------|\n| `GET` | `/customers` | member, admin, support |\n| `GET` | `/customers/:id` | member, admin, support |\n| `POST` | `/customers` | admin |\n| `PATCH` | `/customers/:id` | admin, support |\n| `DELETE` | `/customers/:id` | admin |\n| `GET` | `/customers/views/summary` | member, admin, support |\n| `GET` | `/customers/views/full` | admin |\n\n---\n\n## Key Takeaways\n\n1. **~50 lines of configuration** generates **~500+ lines of production code**\n2. **Security is declarative** - you describe *what* you want, not *how*\n3. **All layers work together** - Firewall → Access → Guards → Masking\n4. **Code is readable and auditable** - no magic, just TypeScript\n5. **Type-safe from definition to API** - Drizzle schema drives everything"
|
|
51
|
+
},
|
|
52
|
+
"cli": {
|
|
53
|
+
"title": "CLI Reference",
|
|
54
|
+
"content": "The Quickback CLI is the fastest way to create, compile, and manage your backend projects.\n\n## Installation\n\n```bash\nnpm install -g @kardoe/quickback\n```\n\n## Commands\n\n### Create a Project\n\n```bash\nquickback create <template> <name>\n```\n\n**Templates:**\n- `cloudflare` - Cloudflare Workers + D1 + Better Auth (free)\n- `bun` - Bun + SQLite + Better Auth (free)\n- `turso` - Turso/LibSQL + Better Auth (pro)\n\n**Example:**\n```bash\nquickback create cloudflare my-app\n```\n\nThis scaffolds a complete project with:\n- `quickback.config.ts` - Project configuration\n- `definitions/features/` - Your table definitions\n- Example todos feature with full security configuration\n\n### Compile Definitions\n\n```bash\nquickback compile\n```\n\nReads your definitions and generates:\n- Database migrations (Drizzle)\n- API route handlers (Hono)\n- TypeScript client SDK\n- OpenAPI specification\n\nRun this after making changes to your definitions.\n\n### Initialize Structure\n\n```bash\nquickback init\n```\n\nCreates the Quickback folder structure in an existing project:\n```\nquickback/\n definitions/\n features/\n auth/\n quickback.config.ts\n```\n\n### View Documentation\n\n```bash\nquickback docs # List available topics\nquickback docs <topic> # Show documentation for a topic\n```\n\n**Available topics:**\n- `firewall` - Data isolation layer\n- `access` - Role-based permissions\n- `guards` - Field protection\n- `masking` - PII redaction\n- `actions` - Custom business logic\n- `api` - CRUD endpoints reference\n- `config` - Configuration reference\n- `features` - Schema definitions\n\nDocumentation is bundled with the CLI and works offline.\n\n### Manage Claude Code Skill\n\n```bash\nquickback claude install # Interactive install\nquickback claude install --global # Install to ~/.claude/skills/\nquickback claude install --local # Install to ./quickback/.claude/\nquickback claude update # Update to latest version\nquickback claude remove # Remove installed skill\nquickback claude status # Check installation status\n```\n\nThe Quickback skill for Claude Code provides AI assistance for:\n- Creating resource definitions with proper security layers\n- Configuring Firewall, Access, Guards, and Masking\n- Debugging configuration issues\n- Understanding security patterns\n\n### Authentication\n\n```bash\nquickback login # Authenticate for Pro templates\nquickback logout # Clear stored credentials\nquickback whoami # Show current auth status\n```\n\n## Quick Start\n\n```bash\n# 1. Create a new project\nquickback create cloudflare my-app\ncd my-app\n\n# 2. Define your tables in definitions/features/\n# (Already has example todos feature)\n\n# 3. Compile\nquickback compile\n\n# 4. Run\nnpm run dev\n```\n\n## Options\n\n| Flag | Description |\n|------|-------------|\n| `-v, --version` | Show version number |\n| `-h, --help` | Show help message |\n\n## Environment Variables\n\n| Variable | Description |\n|----------|-------------|\n| `QUICKBACK_API_KEY` | API key for authentication (alternative to `quickback login`) |\n| `QUICKBACK_API_URL` | Override compiler API URL |\n\n### API Key Authentication\n\nUse an API key instead of interactive login. Useful for CI/CD pipelines:\n\n```bash\n# Pass API key for a single command\nQUICKBACK_API_KEY=your_api_key quickback compile\n\n# Or export for the session\nexport QUICKBACK_API_KEY=your_api_key\nquickback compile\n```\n\nThe API key takes precedence over stored credentials from `quickback login`.\n\n### Custom Compiler URL\n\nPoint the CLI to a different compiler (local or custom):\n\n```bash\n# Use a local compiler\nQUICKBACK_API_URL=http://localhost:3020 quickback compile\n\n# Or export for the session\nexport QUICKBACK_API_URL=http://localhost:3020\nquickback compile\n```\n\nSee [Local Compiler](/compiler-local) for running the compiler locally with Docker.\n\n## Troubleshooting\n\n### \"Command not found: quickback\"\n\nMake sure the CLI is installed globally:\n```bash\nnpm install -g @kardoe/quickback\n```\n\nOr use npx:\n```bash\nnpx @kardoe/quickback create cloudflare my-app\n```\n\n### Compile errors\n\n1. Check your `quickback.config.ts` exists and is valid\n2. Ensure all tables in `definitions/features/` have valid exports\n3. Run `quickback compile` with `--verbose` for detailed output\n\n### Authentication issues\n\nClear credentials and re-authenticate:\n```bash\nquickback logout\nquickback login\n```"
|
|
55
|
+
},
|
|
56
|
+
"cloudflare": {
|
|
57
|
+
"title": "Quickback Stack",
|
|
58
|
+
"content": "Quickback Stack is the production-ready Cloudflare + Better Auth integration that runs entirely on YOUR Cloudflare account.\n\n## What is Quickback Stack?\n\nWhile the [Quickback Compiler](/compiler) transforms your definitions into deployable code, Quickback Stack is the runtime environment where that code runs. It's a complete backend architecture built on Cloudflare's edge platform:\n\n- **Your account, your data** - Everything runs on your Cloudflare account\n- **Edge-first** - Global distribution with sub-50ms latency worldwide\n- **Integrated services** - D1, R2, KV, Workers, and Durable Objects working together\n- **Production-ready auth** - Better Auth with plugins for every use case\n\n## Why Cloudflare?\n\n| Benefit | Description |\n|---------|-------------|\n| **Global Edge** | Your API runs in 300+ data centers worldwide |\n| **Zero Cold Starts** | Workers are always warm, no Lambda-style delays |\n| **Integrated Storage** | D1, R2, and KV are first-class bindings, not external services |\n| **Predictable Pricing** | No surprise bills from serverless scaling |\n| **Single Deployment** | One `wrangler deploy` for everything |\n\n## Architecture Overview\n\n```\n┌─────────────────────────────────────────────────────────────┐\n│ Cloudflare Edge │\n├─────────────────────────────────────────────────────────────┤\n│ │\n│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │\n│ │ Workers │ │ Durable │ │ KV │ │\n│ │ (API) │ │ Objects │ │ (Cache) │ │\n│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │\n│ │ │ │ │\n│ ┌──────┴──────┐ ┌──────┴──────┐ ┌──────┴──────┐ │\n│ │ D1 │ │ Realtime │ │ Sessions │ │\n│ │ (SQLite) │ │ (WebSocket)│ │ (Auth) │ │\n│ └─────────────┘ └─────────────┘ └─────────────┘ │\n│ │\n│ ┌─────────────┐ ┌─────────────────────────────────┐ │\n│ │ R2 │ │ Better Auth │ │\n│ │ (Storage) │ │ (Email OTP, Passkeys, etc.) │ │\n│ └─────────────┘ └─────────────────────────────────┘ │\n│ │\n└─────────────────────────────────────────────────────────────┘\n```\n\n## Stack Components\n\n| Component | Cloudflare Service | Purpose |\n|-----------|-------------------|---------|\n| [D1 Database](/cloudflare/d1-database) | D1 | SQLite at the edge for your data |\n| [File Storage](/cloudflare/file-storage) | R2 | S3-compatible object storage |\n| [KV Storage](/cloudflare/kv-storage) | Workers KV | Key-value for sessions and cache |\n| [Realtime](/cloudflare/realtime) | Durable Objects | WebSocket connections for live updates |\n| [Account App](/cloudflare/account-app) | Workers | User-facing account management UI |\n\n## What You Own\n\nEverything in Quickback Stack runs on your Cloudflare account:\n\n- **Databases** - Your D1 instances, your data\n- **Storage** - Your R2 buckets, your files\n- **Workers** - Your deployments, your logs\n- **Secrets** - Your API keys, stored in your Wrangler secrets\n\nQuickback provides the architecture and code generation. You own the infrastructure.\n\n## Quick Start Checklist\n\n1. **Cloudflare Account** - Sign up at [cloudflare.com](https://cloudflare.com)\n2. **Wrangler CLI** - `npm install -g wrangler`\n3. **Quickback CLI** - `npm install -g @kardoe/quickback`\n4. **Create Project** - `quickback create cloudflare my-app`\n5. **Compile** - `quickback compile`\n6. **Deploy** - `wrangler deploy`\n\n## Comparing Options\n\nNot sure if Cloudflare is right for you? See [Supabase vs Cloudflare](/cloudflare/supabase-vs-cloudflare) for a detailed comparison."
|
|
59
|
+
},
|
|
60
|
+
"d1": {
|
|
61
|
+
"title": "D1 Database",
|
|
62
|
+
"content": "Cloudflare D1 is SQLite at the edge. Quickback uses D1 as the primary database for Cloudflare deployments, with a multi-database pattern for separation of concerns.\n\n## What is D1?\n\nD1 is Cloudflare's serverless SQL database built on SQLite:\n\n- **Edge-native** - Runs in Cloudflare's global network\n- **SQLite compatible** - Use familiar SQL syntax\n- **Zero configuration** - No connection strings or pooling\n- **Automatic replication** - Read replicas at every edge location\n\n## Multi-Database Pattern\n\nQuickback generates separate D1 databases for different concerns:\n\n| Database | Binding | Purpose |\n|----------|---------|---------|\n| `AUTH_DB` | `AUTH_DB` | Better Auth tables (users, sessions, accounts) |\n| `DB` | `DB` | Your application data |\n| `FILES_DB` | `FILES_DB` | File metadata for R2 uploads |\n| `WEBHOOKS_DB` | `WEBHOOKS_DB` | Webhook delivery tracking |\n\nThis separation provides:\n- **Independent scaling** - Auth traffic doesn't affect app queries\n- **Isolation** - Auth schema changes don't touch your data\n- **Clarity** - Clear ownership of each database\n\n## Drizzle ORM Integration\n\nQuickback uses [Drizzle ORM](https://orm.drizzle.team/) for type-safe database access:\n\n```ts\n// schema/tables.ts - Your schema definition\n\nexport const posts = sqliteTable(\"posts\", {\n id: text(\"id\").primaryKey(),\n title: text(\"title\").notNull(),\n content: text(\"content\"),\n authorId: text(\"author_id\").notNull(),\n createdAt: integer(\"created_at\", { mode: \"timestamp\" }),\n});\n```\n\nThe compiler generates Drizzle queries based on your security rules:\n\n```ts\n// Generated query with firewall applied\nconst result = await db\n .select()\n .from(posts)\n .where(eq(posts.authorId, userId)); // Firewall injects ownership\n```\n\n## Migrations\n\nQuickback generates migrations automatically at compile time based on your schema changes:\n\n```bash\n# Compile your project (generates migrations)\nquickback compile\n\n# Apply migrations locally\nwrangler d1 migrations apply DB --local\n\n# Apply to production D1\nwrangler d1 migrations apply DB --remote\n```\n\nMigration files are generated in `drizzle/migrations/` and version-controlled with your code. You never need to manually generate migrations—just define your schema and compile.\n\n## wrangler.toml Bindings\n\nConfigure D1 bindings in `wrangler.toml`:\n\n```toml\n[[d1_databases]]\nbinding = \"DB\"\ndatabase_name = \"my-app-db\"\ndatabase_id = \"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\"\n\n[[d1_databases]]\nbinding = \"AUTH_DB\"\ndatabase_name = \"my-app-auth\"\ndatabase_id = \"yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy\"\n\n[[d1_databases]]\nbinding = \"FILES_DB\"\ndatabase_name = \"my-app-files\"\ndatabase_id = \"zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz\"\n```\n\nCreate databases via Wrangler:\n\n```bash\nwrangler d1 create my-app-db\nwrangler d1 create my-app-auth\nwrangler d1 create my-app-files\n```\n\n## Accessing Data via API\n\nAll data access in Quickback goes through the generated API endpoints—never direct database queries. This ensures security rules (firewall, access, guards, masking) are always enforced.\n\n### CRUD Operations\n\n```bash\n# List posts (firewall automatically filters by ownership)\nGET /api/v1/posts\n\n# Get a single post\nGET /api/v1/posts/:id\n\n# Create a post (guards validate allowed fields)\nPOST /api/v1/posts\n{ \"title\": \"Hello World\", \"content\": \"...\" }\n\n# Update a post (guards validate updatable fields)\nPATCH /api/v1/posts/:id\n{ \"title\": \"Updated Title\" }\n\n# Delete a post\nDELETE /api/v1/posts/:id\n```\n\n### Filtering and Pagination\n\n```bash\n# Filter by field\nGET /api/v1/posts?status=published\n\n# Pagination\nGET /api/v1/posts?limit=10&offset=20\n\n# Sort\nGET /api/v1/posts?sort=createdAt&order=desc\n```\n\n### Why No Direct Database Access?\n\nDirect database queries bypass Quickback's security layers:\n- **Firewall** - Data isolation by user/org/team\n- **Access** - Role-based permissions\n- **Guards** - Field-level create/update restrictions\n- **Masking** - Sensitive data redaction\n\nAlways use the API endpoints. For custom business logic, use [Actions](/actions).\n\n## Local Development\n\nD1 works locally with Wrangler:\n\n```bash\n# Start local dev server with D1\nwrangler dev\n\n# D1 data persists in .wrangler/state/\n```\n\nLocal D1 uses SQLite files in `.wrangler/state/v3/d1/`, which you can inspect with any SQLite client.\n\n## Security Architecture\n\nD1 uses application-layer security that is equally secure to Supabase RLS when using Quickback-generated code. The key difference is where enforcement happens.\n\n### How Security Works\n\n| Component | Enforcement | Notes |\n|-----------|-------------|-------|\n| CRUD endpoints | ✅ Firewall auto-applied | All generated routes enforce security |\n| Actions | ✅ Firewall auto-applied | Both standalone and record-based |\n| Manual routes | ⚠️ Must apply firewall | Use `withFirewall` helper |\n\n### Why D1 is Secure\n\n1. **No external database access** - D1 can only be queried through your Worker. There's no connection string or external endpoint.\n2. **Generated code enforces rules** - All CRUD and Action endpoints automatically apply firewall, access, guards, and masking.\n3. **Single entry point** - Every request flows through your API where security is enforced.\n\nUnlike Supabase where PostgreSQL RLS provides database-level enforcement, D1's security comes from architecture: the database is inaccessible except through your Worker, and all generated routes apply the four security pillars.\n\n### Comparison with Supabase RLS\n\n| Scenario | Supabase | D1 |\n|----------|----------|-----|\n| CRUD endpoints | ✅ Secure (RLS + App) | ✅ Secure (App) |\n| Actions | ✅ Secure (RLS + App) | ✅ Secure (App) |\n| Manual routes | ✅ RLS still protects | ⚠️ Must apply firewall |\n| External DB access | ⚠️ Possible with credentials | ✅ Not possible |\n| Dashboard queries | Via Supabase Studio | ⚠️ Admin only (audit logged) |\n\n### Writing Manual Routes\n\nIf you write custom routes outside of Quickback compilation (e.g., custom reports, integrations), use the generated `withFirewall` helper to ensure security:\n\n```ts\n\napp.get('/reports/monthly', async (c) => {\n return withFirewall(c, async (ctx, firewall) => {\n const results = await db.select()\n .from(invoices)\n .where(firewall);\n return c.json(results);\n });\n});\n```\n\nThe `withFirewall` helper:\n- Validates authentication\n- Builds the correct WHERE conditions for the current user/org\n- Returns 401 if not authenticated\n\n### Best Practices\n\n1. **Use generated endpoints** - Prefer CRUD and Actions over manual routes\n2. **Always apply firewall** - When writing manual routes, always use `withFirewall`\n3. **Avoid raw SQL** - Raw SQL bypasses application security; use Drizzle ORM\n4. **Review custom code** - Manual routes should be code-reviewed for security\n\n## Limitations\n\nD1 is built on SQLite, which means:\n\n- **No stored procedures** - Business logic lives in your Worker\n- **Single-writer** - One write connection at a time (reads scale horizontally)\n- **Size limits** - 10GB per database (free tier: 500MB)\n- **No PostgreSQL extensions** - Use Supabase if you need PostGIS, etc.\n\nFor most applications, these limits are non-issues. D1's edge distribution and zero-config setup outweigh the constraints."
|
|
63
|
+
},
|
|
64
|
+
"reference": {
|
|
65
|
+
"title": "Quick Reference",
|
|
66
|
+
"content": "A comprehensive reference of all Quickback definition options.\n\n## Layer Summary\n\n| Layer | Purpose | Key Options |\n|-------|---------|-------------|\n| **Firewall** | Data isolation | `owner`, `organization`, `team`, `softDelete`, `exception` |\n| **Access** | Who can do what | `roles`, `record` conditions, `or`/`and`, `$ctx.` variables |\n| **Guards** | Field protection | `createable`, `updatable`, `protected`, `immutable`, `false` |\n| **Masking** | Field redaction | `email`, `phone`, `ssn`, `creditCard`, `name`, `redact`, `custom` |\n| **Actions** | Custom routes | `guard`, `input`, `execute`/`handler`, `standalone` |\n| **CRUD List** | Query options | `pageSize`, `maxPageSize`, `fields`, filtering, sorting |\n| **PUT/Upsert** | External sync | Requires `guards: false` + `generateId: false` |\n| **Batch Ops** | Bulk operations | Auto-enabled for CREATE/UPDATE/DELETE, `maxBatchSize`, `atomic` mode |\n\n## Database Config Options\n\n```typescript\n// quickback.config.ts\nexport default {\n database: {\n generateId: 'uuid', // 'uuid' | 'cuid' | 'nanoid' | 'serial' | false\n namingConvention: 'camelCase', // 'camelCase' | 'snake_case'\n usePlurals: true, // Table names: 'users' vs 'user'\n },\n compiler: {\n features: {\n auditFields: true, // Auto-manage createdAt/By, modifiedAt/By\n }\n }\n};\n```\n\n## Firewall Options\n\n```typescript\nfirewall: {\n owner?: {\n column?: string;\n source?: string;\n mode?: 'required' | 'optional';\n };\n organization?: {\n column?: string;\n source?: string;\n };\n team?: {\n column?: string;\n source?: string;\n };\n softDelete?: {\n column?: string;\n };\n exception?: boolean;\n}\n```\n\n## Access Conditions\n\n```typescript\n// Field conditions\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// Combinators\n{ or: Access[] }\n{ and: Access[] }\n```\n\n## Query Parameters\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## Guards Lists\n\n| List | What it controls |\n|------|------------------|\n| `createable` | Fields allowed in create (POST) body |\n| `updatable` | Fields allowed in update (PATCH) body |\n| `protected` | Only modifiable via named actions (not in createable/updatable) |\n| `immutable` | Set on create, blocked on update (not in updatable) |\n\nFields can be in both `createable` and `updatable`. System fields are always auto-managed.\n\n## Mask Types\n\n| Type | Example Input | Masked Output |\n|------|---------------|---------------|\n| `'email'` | `john@yourdomain.com` | `j***@y***.com` |\n| `'phone'` | `555-123-4567` | `***-***-4567` |\n| `'ssn'` | `123-45-6789` | `***-**-6789` |\n| `'creditCard'` | `4111111111111111` | `************1111` |\n| `'name'` | `John Smith` | `J*** S***` |\n| `'redact'` | `anything` | `[REDACTED]` |\n| `'custom'` | (your logic) | (your output) |\n\n## ID Generation Options\n\n| `generateId` | PUT Available? | Notes |\n|--------------|----------------|-------|\n| `'uuid'` | No | Server generates UUID |\n| `'cuid'` | No | Server generates CUID |\n| `'nanoid'` | No | Server generates nanoid |\n| `'serial'` | No | Database auto-increments |\n| `false` | Yes (if guards: false) | Client provides ID |\n\n## Batch Operations\n\n**Auto-enabled** when corresponding CRUD operations exist:\n\n| Endpoint | Auto-enabled when | Default Limit |\n|----------|-------------------|---------------|\n| `POST /batch` | `crud.create` exists | 100 records |\n| `PATCH /batch` | `crud.update` exists | 100 records |\n| `DELETE /batch` | `crud.delete` exists | 100 records |\n| `PUT /batch` | `crud.put` exists (guards: false + generateId: false) | 100 records |\n\n**Batch options:**\n```typescript\ncrud: {\n batchCreate: {\n access: { roles: ['admin'] }, // Override CRUD access\n maxBatchSize: 50, // Custom limit\n allowAtomic: false // Disable atomic mode\n }\n}\n```\n\n**Request format:**\n```json\n{\n \"records\": [ /* array of objects */ ],\n \"options\": { \"atomic\": false }\n}\n```\n\n**Response modes:**\n- **Partial success** (default): Returns `{ success: [], errors: [], meta: {} }` with HTTP 207/201/200\n- **Atomic** (opt-in): All-or-nothing transaction, returns HTTP 400 on any error\n\nSee [CRUD Endpoints - Batch Operations](/crud-endpoints#batch-operations) for details.\n\n## LLM Documentation\n\nThis documentation is optimized for AI assistants and large language models.\n\n| Resource | URL | Description |\n|----------|-----|-------------|\n| Summary | [quickback.dev/llm.md](https://quickback.dev/llm.md) | Concise overview for AI assistants |\n| Copy Button | On each page | Click \"Copy Markdown\" to copy the current page |\n\nUse the summary file for quick context or the copy button to get the full markdown for any specific page."
|
|
67
|
+
},
|
|
68
|
+
"compiler-local": {
|
|
69
|
+
"title": "Local Compiler",
|
|
70
|
+
"content": "Run the Quickback compiler locally using Docker Desktop. This is for select users with local compiler access.\n\n## Prerequisites\n\n- Docker Desktop installed and running\n- Local compiler image access (contact support)\n\n## Setup\n\nPull and run the compiler container:\n\n```bash\ndocker run -d -p 3020:3020 --name quickback-compiler ghcr.io/kardoe/quickback-compiler:latest\n```\n\n## Usage\n\nPoint the CLI to your local compiler:\n\n```bash\nQUICKBACK_API_URL=http://localhost:3020 quickback compile\n```\n\nOr export it for the session:\n\n```bash\nexport QUICKBACK_API_URL=http://localhost:3020\nquickback compile\n```\n\n### With API Key\n\nIf your local compiler requires authentication:\n\n```bash\nQUICKBACK_API_URL=http://localhost:3020 QUICKBACK_API_KEY=your_key quickback compile\n```\n\n## Verify\n\nCheck the compiler is running:\n\n```bash\ncurl http://localhost:3020/health\n```\n\n## Ports\n\nThe default port is `3020`. If you need a different port:\n\n```bash\ndocker run -d -p 3000:3020 --name quickback-compiler ghcr.io/kardoe/quickback-compiler:latest\nQUICKBACK_API_URL=http://localhost:3000 quickback compile\n```\n\n## Troubleshooting\n\n### Container not starting\n\n```bash\ndocker logs quickback-compiler\n```\n\n### Port already in use\n\n```bash\n# Find what's using the port\nlsof -i :3020\n\n# Use a different port\ndocker run -d -p 3021:3020 --name quickback-compiler ghcr.io/kardoe/quickback-compiler:latest\n```\n\n### Stop and remove\n\n```bash\ndocker stop quickback-compiler\ndocker rm quickback-compiler\n```"
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
export const TOPIC_LIST = [
|
|
74
|
+
"config",
|
|
75
|
+
"features",
|
|
76
|
+
"firewall",
|
|
77
|
+
"access",
|
|
78
|
+
"guards",
|
|
79
|
+
"masking",
|
|
80
|
+
"actions",
|
|
81
|
+
"views",
|
|
82
|
+
"api",
|
|
83
|
+
"quickstart",
|
|
84
|
+
"concepts",
|
|
85
|
+
"examples",
|
|
86
|
+
"cli",
|
|
87
|
+
"cloudflare",
|
|
88
|
+
"d1",
|
|
89
|
+
"reference",
|
|
90
|
+
"compiler-local"
|
|
91
|
+
];
|
|
92
|
+
//# sourceMappingURL=content.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content.js","sourceRoot":"","sources":["../../src/docs/content.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,0DAA0D;AAO1D,MAAM,CAAC,MAAM,IAAI,GAA6B;IAC5C,QAAQ,EAAE;QACR,OAAO,EAAE,UAAU;QACnB,SAAS,EAAE,46gBAA46gB;KACx7gB;IACD,UAAU,EAAE;QACV,OAAO,EAAE,iBAAiB;QAC1B,SAAS,EAAE,sgSAAsgS;KAClhS;IACD,UAAU,EAAE;QACV,OAAO,EAAE,2BAA2B;QACpC,SAAS,EAAE,wmKAAwmK;KACpnK;IACD,QAAQ,EAAE;QACR,OAAO,EAAE,gDAAgD;QACzD,SAAS,EAAE,+5LAA+5L;KAC36L;IACD,QAAQ,EAAE;QACR,OAAO,EAAE,mCAAmC;QAC5C,SAAS,EAAE,+zMAA+zM;KAC30M;IACD,SAAS,EAAE;QACT,OAAO,EAAE,2BAA2B;QACpC,SAAS,EAAE,ioJAAioJ;KAC7oJ;IACD,SAAS,EAAE;QACT,OAAO,EAAE,SAAS;QAClB,SAAS,EAAE,w4XAAw4X;KACp5X;IACD,OAAO,EAAE;QACP,OAAO,EAAE,+BAA+B;QACxC,SAAS,EAAE,8xOAA8xO;KAC1yO;IACD,KAAK,EAAE;QACL,OAAO,EAAE,gBAAgB;QACzB,SAAS,EAAE,glgBAAglgB;KAC5lgB;IACD,YAAY,EAAE;QACZ,OAAO,EAAE,aAAa;QACtB,SAAS,EAAE,o/JAAo/J;KAChgK;IACD,UAAU,EAAE;QACV,OAAO,EAAE,0BAA0B;QACnC,SAAS,EAAE,kxPAAkxP;KAC9xP;IACD,UAAU,EAAE;QACV,OAAO,EAAE,kBAAkB;QAC3B,SAAS,EAAE,4jYAA4jY;KACxkY;IACD,KAAK,EAAE;QACL,OAAO,EAAE,eAAe;QACxB,SAAS,EAAE,i6IAAi6I;KAC76I;IACD,YAAY,EAAE;QACZ,OAAO,EAAE,iBAAiB;QAC1B,SAAS,EAAE,o2HAAo2H;KACh3H;IACD,IAAI,EAAE;QACJ,OAAO,EAAE,aAAa;QACtB,SAAS,EAAE,4vOAA4vO;KACxwO;IACD,WAAW,EAAE;QACX,OAAO,EAAE,iBAAiB;QAC1B,SAAS,EAAE,q6KAAq6K;KACj7K;IACD,gBAAgB,EAAE;QAChB,OAAO,EAAE,gBAAgB;QACzB,SAAS,EAAE,ihDAAihD;KAC7hD;CACF,CAAC;AAEF,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,QAAQ;IACR,UAAU;IACV,UAAU;IACV,QAAQ;IACR,QAAQ;IACR,SAAS;IACT,SAAS;IACT,OAAO;IACP,KAAK;IACL,YAAY;IACZ,UAAU;IACV,UAAU;IACV,KAAK;IACL,YAAY;IACZ,IAAI;IACJ,WAAW;IACX,gBAAgB;CACjB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,8 @@ import { create } from "./commands/create.js";
|
|
|
17
17
|
import { login } from "./commands/login.js";
|
|
18
18
|
import { logout } from "./commands/logout.js";
|
|
19
19
|
import { whoami } from "./commands/whoami.js";
|
|
20
|
+
import { docs } from "./commands/docs.js";
|
|
21
|
+
import { claude } from "./commands/claude.js";
|
|
20
22
|
// Get package version from package.json
|
|
21
23
|
function getPackageVersion() {
|
|
22
24
|
try {
|
|
@@ -46,6 +48,8 @@ ${pc.bold("COMMANDS")}
|
|
|
46
48
|
${pc.cyan("create")} <template> <name> Create a new project from template
|
|
47
49
|
${pc.cyan("compile")} Compile definitions via remote API
|
|
48
50
|
${pc.cyan("init")} Initialize quickback folder structure
|
|
51
|
+
${pc.cyan("docs")} [topic] Show built-in documentation
|
|
52
|
+
${pc.cyan("claude")} <command> Manage Claude Code skill
|
|
49
53
|
${pc.cyan("login")} Authenticate for Pro templates
|
|
50
54
|
${pc.cyan("logout")} Clear stored credentials
|
|
51
55
|
${pc.cyan("whoami")} Show current auth status
|
|
@@ -67,6 +71,12 @@ ${pc.bold("EXAMPLES")}
|
|
|
67
71
|
${pc.gray("# List available templates")}
|
|
68
72
|
quickback create --list
|
|
69
73
|
|
|
74
|
+
${pc.gray("# View documentation")}
|
|
75
|
+
quickback docs firewall
|
|
76
|
+
|
|
77
|
+
${pc.gray("# Install Claude Code skill")}
|
|
78
|
+
quickback claude install
|
|
79
|
+
|
|
70
80
|
${pc.bold("TEMPLATES")}
|
|
71
81
|
${pc.cyan("cloudflare")} Cloudflare Workers + D1 + Better Auth (free)
|
|
72
82
|
${pc.cyan("bun")} Bun + SQLite + Better Auth (free)
|
|
@@ -122,6 +132,16 @@ else if (cmd === "whoami") {
|
|
|
122
132
|
fatal(String(err?.message ?? err));
|
|
123
133
|
});
|
|
124
134
|
}
|
|
135
|
+
else if (cmd === "docs") {
|
|
136
|
+
docs(args).catch((err) => {
|
|
137
|
+
fatal(String(err?.message ?? err));
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
else if (cmd === "claude") {
|
|
141
|
+
claude(args).catch((err) => {
|
|
142
|
+
fatal(String(err?.message ?? err));
|
|
143
|
+
});
|
|
144
|
+
}
|
|
125
145
|
else if (cmd === "db:generate" || cmd === "db:migrate" || cmd === "db:studio" || cmd === "migrate") {
|
|
126
146
|
// Legacy database commands - now handled via drizzle-kit directly
|
|
127
147
|
console.log(pc.yellow(`\nThe '${cmd}' command has been deprecated.`));
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;GAQG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B,OAAO,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAChD,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAO9C,wCAAwC;AACxC,SAAS,iBAAiB;IACtB,IAAI,CAAC;QACD,MAAM,WAAW,GAAG,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,cAAc,CAAC,CAAC;QAC1D,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,WAAW,EAAE,MAAM,CAAC,CAAe,CAAC;QAChF,OAAO,WAAW,CAAC,OAAiB,CAAC;IACzC,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,OAAO,CAAC;IACnB,CAAC;AACL,CAAC;AAED,4BAA4B;AAC5B,SAAS,YAAY;IACjB,MAAM,OAAO,GAAG,iBAAiB,EAAE,CAAC;IACpC,OAAO,CAAC,GAAG,CAAC,mBAAmB,OAAO,EAAE,CAAC,CAAC;AAC9C,CAAC;AAED,yBAAyB;AACzB,SAAS,SAAS;IACd,MAAM,OAAO,GAAG,iBAAiB,EAAE,CAAC;IACpC,MAAM,IAAI,GAAG;EACf,EAAE,CAAC,IAAI,CAAC,eAAe,CAAC,KAAK,OAAO;;EAEpC,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC;;;EAGhB,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC;IACjB,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC;IACjB,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC;IAClB,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC;IACf,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC;IAChB,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC;IACjB,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC;;EAEnB,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC;IAChB,EAAE,CAAC,IAAI,CAAC,eAAe,CAAC;IACxB,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC;;EAEvB,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC;IACjB,EAAE,CAAC,IAAI,CAAC,mCAAmC,CAAC;;;IAG5C,EAAE,CAAC,IAAI,CAAC,8BAA8B,CAAC;;;IAGvC,EAAE,CAAC,IAAI,CAAC,4BAA4B,CAAC;;;IAGrC,EAAE,CAAC,IAAI,CAAC,4BAA4B,CAAC;;;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;GAQG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B,OAAO,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAChD,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAO9C,wCAAwC;AACxC,SAAS,iBAAiB;IACtB,IAAI,CAAC;QACD,MAAM,WAAW,GAAG,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,cAAc,CAAC,CAAC;QAC1D,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,WAAW,EAAE,MAAM,CAAC,CAAe,CAAC;QAChF,OAAO,WAAW,CAAC,OAAiB,CAAC;IACzC,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,OAAO,CAAC;IACnB,CAAC;AACL,CAAC;AAED,4BAA4B;AAC5B,SAAS,YAAY;IACjB,MAAM,OAAO,GAAG,iBAAiB,EAAE,CAAC;IACpC,OAAO,CAAC,GAAG,CAAC,mBAAmB,OAAO,EAAE,CAAC,CAAC;AAC9C,CAAC;AAED,yBAAyB;AACzB,SAAS,SAAS;IACd,MAAM,OAAO,GAAG,iBAAiB,EAAE,CAAC;IACpC,MAAM,IAAI,GAAG;EACf,EAAE,CAAC,IAAI,CAAC,eAAe,CAAC,KAAK,OAAO;;EAEpC,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC;;;EAGhB,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC;IACjB,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC;IACjB,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC;IAClB,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC;IACf,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC;IACf,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC;IACjB,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC;IAChB,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC;IACjB,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC;;EAEnB,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC;IAChB,EAAE,CAAC,IAAI,CAAC,eAAe,CAAC;IACxB,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC;;EAEvB,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC;IACjB,EAAE,CAAC,IAAI,CAAC,mCAAmC,CAAC;;;IAG5C,EAAE,CAAC,IAAI,CAAC,8BAA8B,CAAC;;;IAGvC,EAAE,CAAC,IAAI,CAAC,4BAA4B,CAAC;;;IAGrC,EAAE,CAAC,IAAI,CAAC,4BAA4B,CAAC;;;IAGrC,EAAE,CAAC,IAAI,CAAC,sBAAsB,CAAC;;;IAG/B,EAAE,CAAC,IAAI,CAAC,6BAA6B,CAAC;;;EAGxC,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC;IAClB,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC;IACrB,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC;IACd,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC;;EAElB,EAAE,CAAC,IAAI,CAAC,eAAe,CAAC;;CAEzB,CAAC;IACE,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AACtB,CAAC;AAED,sBAAsB;AACtB,SAAS,KAAK,CAAC,OAAe;IAC1B,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,OAAO,OAAO,EAAE,CAAC,CAAC,CAAC;IACxC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACpB,CAAC;AAED,uBAAuB;AACvB,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAC5B,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAEnC,yBAAyB;AACzB,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,WAAW,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;IAC3D,YAAY,EAAE,CAAC;AACnB,CAAC;KAAM,IAAI,GAAG,KAAK,MAAM,IAAI,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;IACpE,SAAS,EAAE,CAAC;AAChB,CAAC;KAAM,IAAI,GAAG,KAAK,MAAM,EAAE,CAAC;IACxB,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;QACjB,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE,OAAO,IAAI,GAAG,CAAC,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;AACP,CAAC;KAAM,IAAI,GAAG,KAAK,QAAQ,EAAE,CAAC;IAC1B,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;QACvB,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE,OAAO,IAAI,GAAG,CAAC,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;AACP,CAAC;KAAM,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;IAC3B,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;QACpB,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE,OAAO,IAAI,GAAG,CAAC,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;AACP,CAAC;KAAM,IAAI,GAAG,KAAK,OAAO,EAAE,CAAC;IACzB,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;QAClB,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE,OAAO,IAAI,GAAG,CAAC,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;AACP,CAAC;KAAM,IAAI,GAAG,KAAK,QAAQ,EAAE,CAAC;IAC1B,MAAM,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;QACnB,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE,OAAO,IAAI,GAAG,CAAC,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;AACP,CAAC;KAAM,IAAI,GAAG,KAAK,QAAQ,EAAE,CAAC;IAC1B,MAAM,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;QACnB,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE,OAAO,IAAI,GAAG,CAAC,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;AACP,CAAC;KAAM,IAAI,GAAG,KAAK,MAAM,EAAE,CAAC;IACxB,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;QACrB,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE,OAAO,IAAI,GAAG,CAAC,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;AACP,CAAC;KAAM,IAAI,GAAG,KAAK,QAAQ,EAAE,CAAC;IAC1B,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;QACvB,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE,OAAO,IAAI,GAAG,CAAC,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;AACP,CAAC;KAAM,IAAI,GAAG,KAAK,aAAa,IAAI,GAAG,KAAK,YAAY,IAAI,GAAG,KAAK,WAAW,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;IACnG,kEAAkE;IAClE,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,CAAC,UAAU,GAAG,gCAAgC,CAAC,CAAC,CAAC;IACtE,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,+CAA+C,CAAC,CAAC,CAAC;IACtE,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,qDAAqD,CAAC,CAAC,CAAC;IAC5E,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,kDAAkD,CAAC,CAAC,CAAC;IACzE,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,sDAAsD,CAAC,CAAC,CAAC;IAC7E,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAChB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACpB,CAAC;KAAM,CAAC;IACJ,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,oBAAoB,GAAG,EAAE,CAAC,CAAC,CAAC;IACjD,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,kDAAkD,CAAC,CAAC,CAAC;IACzE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACpB,CAAC"}
|
package/dist/lib/auth.d.ts
CHANGED
|
@@ -21,7 +21,11 @@ interface Credentials {
|
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
|
-
* Get
|
|
24
|
+
* Get auth token from environment variable or stored credentials
|
|
25
|
+
*
|
|
26
|
+
* Priority:
|
|
27
|
+
* 1. QUICKBACK_API_KEY environment variable (for CI/CD or headless use)
|
|
28
|
+
* 2. Stored credentials from ~/.quickback/credentials.json (from `quickback login`)
|
|
25
29
|
*/
|
|
26
30
|
export declare function getStoredToken(): Promise<string | null>;
|
|
27
31
|
/**
|
package/dist/lib/auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/lib/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AASH,UAAU,WAAW;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE;QACL,EAAE,EAAE,MAAM,CAAC;QACX,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;CACH;AAED
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/lib/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AASH,UAAU,WAAW;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE;QACL,EAAE,EAAE,MAAM,CAAC;QACX,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;CACH;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA8B7D;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAO9E;AAED;;GAEG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAItD;AAED;;GAEG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,CAazE;AAED;;GAEG;AACH,wBAAsB,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC,CAGnD"}
|
package/dist/lib/auth.js
CHANGED
|
@@ -17,9 +17,19 @@ import { join } from 'path';
|
|
|
17
17
|
const CONFIG_DIR = join(homedir(), '.quickback');
|
|
18
18
|
const CREDENTIALS_FILE = join(CONFIG_DIR, 'credentials.json');
|
|
19
19
|
/**
|
|
20
|
-
* Get
|
|
20
|
+
* Get auth token from environment variable or stored credentials
|
|
21
|
+
*
|
|
22
|
+
* Priority:
|
|
23
|
+
* 1. QUICKBACK_API_KEY environment variable (for CI/CD or headless use)
|
|
24
|
+
* 2. Stored credentials from ~/.quickback/credentials.json (from `quickback login`)
|
|
21
25
|
*/
|
|
22
26
|
export async function getStoredToken() {
|
|
27
|
+
// Check for API key in environment first
|
|
28
|
+
const apiKey = process.env.QUICKBACK_API_KEY;
|
|
29
|
+
if (apiKey) {
|
|
30
|
+
return apiKey;
|
|
31
|
+
}
|
|
32
|
+
// Fall back to stored credentials
|
|
23
33
|
try {
|
|
24
34
|
if (!existsSync(CREDENTIALS_FILE)) {
|
|
25
35
|
return null;
|
package/dist/lib/auth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/lib/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AACpF,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAC7B,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,YAAY,CAAC,CAAC;AACjD,MAAM,gBAAgB,GAAG,IAAI,CAAC,UAAU,EAAE,kBAAkB,CAAC,CAAC;AAY9D
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/lib/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AACpF,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAC7B,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,YAAY,CAAC,CAAC;AACjD,MAAM,gBAAgB,GAAG,IAAI,CAAC,UAAU,EAAE,kBAAkB,CAAC,CAAC;AAY9D;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc;IAClC,yCAAyC;IACzC,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC;IAC7C,IAAI,MAAM,EAAE,CAAC;QACX,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,kCAAkC;IAClC,IAAI,CAAC;QACH,IAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC,EAAE,CAAC;YAClC,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,IAAI,GAAG,YAAY,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAC;QACrD,MAAM,WAAW,GAAgB,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAElD,4BAA4B;QAC5B,IAAI,WAAW,CAAC,SAAS,EAAE,CAAC;YAC1B,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;YAClD,IAAI,SAAS,GAAG,IAAI,IAAI,EAAE,EAAE,CAAC;gBAC3B,0BAA0B;gBAC1B,MAAM,gBAAgB,EAAE,CAAC;gBACzB,OAAO,IAAI,CAAC;YACd,CAAC;QACH,CAAC;QAED,OAAO,WAAW,CAAC,KAAK,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,WAAwB;IAC7D,iCAAiC;IACjC,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC5B,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7C,CAAC;IAED,aAAa,CAAC,gBAAgB,EAAE,IAAI,CAAC,SAAS,CAAC,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;AACjF,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB;IACpC,IAAI,UAAU,CAAC,gBAAgB,CAAC,EAAE,CAAC;QACjC,UAAU,CAAC,gBAAgB,CAAC,CAAC;IAC/B,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa;IACjC,IAAI,CAAC;QACH,IAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC,EAAE,CAAC;YAClC,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,IAAI,GAAG,YAAY,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAC;QACrD,MAAM,WAAW,GAAgB,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAElD,OAAO,WAAW,CAAC,IAAI,IAAI,IAAI,CAAC;IAClC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU;IAC9B,MAAM,KAAK,GAAG,MAAM,cAAc,EAAE,CAAC;IACrC,OAAO,KAAK,KAAK,IAAI,CAAC;AACxB,CAAC"}
|
|
@@ -38,6 +38,13 @@ export declare function defineStorage(name: string, config?: Record<string, unkn
|
|
|
38
38
|
name: string;
|
|
39
39
|
config: Record<string, unknown>;
|
|
40
40
|
};
|
|
41
|
+
/**
|
|
42
|
+
* Stub for defineFileStorage - returns provider config for R2 file storage
|
|
43
|
+
*/
|
|
44
|
+
export declare function defineFileStorage(name: string, config?: Record<string, unknown>): {
|
|
45
|
+
name: string;
|
|
46
|
+
config: Record<string, unknown>;
|
|
47
|
+
};
|
|
41
48
|
/**
|
|
42
49
|
* Stub for defineResource - just returns the config with schema reference
|
|
43
50
|
*/
|
|
@@ -63,4 +70,19 @@ export declare function defineActions<TSchema>(schema: TSchema, actions: Record<
|
|
|
63
70
|
* Stub for defineAction - returns a curried function that returns the config
|
|
64
71
|
*/
|
|
65
72
|
export declare function defineAction<TSchema = unknown>(): (config: Record<string, unknown>) => Record<string, unknown>;
|
|
73
|
+
/**
|
|
74
|
+
* Stub for defineQueue - returns the queue handler definition
|
|
75
|
+
* Used in definitions/services/queues/*.ts
|
|
76
|
+
*/
|
|
77
|
+
export declare function defineQueue<TMessage = unknown>(config: Record<string, unknown>): Record<string, unknown>;
|
|
78
|
+
/**
|
|
79
|
+
* Stub for defineRealtime - returns the realtime definition
|
|
80
|
+
* Used in definitions/services/realtime/*.ts
|
|
81
|
+
*/
|
|
82
|
+
export declare function defineRealtime(config: Record<string, unknown>): Record<string, unknown>;
|
|
83
|
+
/**
|
|
84
|
+
* Stub for defineEmbedding - returns the embedding definition
|
|
85
|
+
* Used in definitions/services/embeddings/*.ts
|
|
86
|
+
*/
|
|
87
|
+
export declare function defineEmbedding(config: Record<string, unknown>): Record<string, unknown>;
|
|
66
88
|
//# sourceMappingURL=compiler-stubs.d.ts.map
|