# Quickback

Quickback is two things:

1. **Compiler** — Transforms declarative TypeScript definitions into secure, production-ready APIs
2. **Stack** — A Supabase alternative running entirely on Cloudflare (D1, R2, KV, Durable Objects, Queues, Workers AI)

The output is standard TypeScript (Hono, Drizzle, Better Auth) running on your own infrastructure.

## Project Structure

```
my-app/
├── quickback/
│   ├── quickback.config.ts
│   └── features/
│       └── {feature-name}/
│           ├── {table}.ts        # Schema + security (defineTable)
│           ├── actions/          # One file per action — defineAction({...}) (optional)
│           │   ├── {action}.ts             # Binds to primary table (or standalone if path: is set)
│           │   └── {table}/{action}.ts     # Binds to sibling <table>.ts (multi-table features)
│           └── lib/              # Feature-local helpers, copied verbatim (optional)
└── ...
```

## quickback.config.ts

```typescript
import { defineConfig, defineRuntime, defineDatabase, defineAuth } from '@quickback/compiler';

export default defineConfig({
  name: "my-saas-app",
  providers: {
    runtime: defineRuntime("cloudflare"),
    database: defineDatabase("cloudflare-d1"),
    auth: defineAuth("better-auth"),
  },
});
```

## Automatic Audit Fields

Quickback auto-injects: `createdAt`, `modifiedAt`, `deletedAt`, `createdBy`, `modifiedBy`, `deletedBy`. Do NOT define these in schemas.

## defineTable() — Schema + Security

Each feature file exports a Drizzle table and a `defineTable()` default export with security config:

```typescript
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
import { defineTable } from "@quickback/compiler";

export const todos = sqliteTable("todos", {
  id: integer("id").primaryKey(),
  title: text("title").notNull(),
  description: text("description"),
  completed: integer("completed", { mode: "boolean" }).default(false),
  userId: text("user_id").notNull(),
  organizationId: text("organization_id").notNull(),
});

export default defineTable(todos, {
  firewall: [
    { field: "organizationId", equals: "ctx.activeOrgId" },
    { field: "userId",         equals: "ctx.userId" },
    { field: "deletedAt",      isNull: true },
  ],
  read: { access: { roles: ["member", "admin"] } },
  create: { access: { roles: ["member", "admin"] } },
  update: { access: { roles: ["admin"] } },
  delete: { access: { roles: ["admin"] }, mode: "soft" },
  guards: {
    createable: ["title", "description", "completed"],
    updatable: ["title", "description", "completed"],
  },
  masking: {
    userId: { type: "redact", show: { roles: ["admin"] } },
  },
});
```

## Four Security Layers

Every request passes through: `Request → Firewall → Access → Guards → Database → Masking → Response`

### 1. Firewall — Data Isolation

Auto WHERE clauses for tenant isolation. Predicates are AND'd:

```typescript
firewall: [
  { field: 'organizationId', equals: 'ctx.activeOrgId' },  // WHERE organizationId = ctx.activeOrgId
  { field: 'ownerId',        equals: 'ctx.userId' },       // AND ownerId = ctx.userId
  { field: 'teamId',         equals: 'ctx.activeTeamId' }, // AND teamId = ctx.activeTeamId
  { field: 'deletedAt',      isNull: true },               // AND deletedAt IS NULL
]

// Public/system resources opt out of tenant scoping (soft-delete still applied):
firewall: [{ exception: true }]
```

Auto-derived when omitted: scans the schema for `organizationId` / `ownerId` / `teamId` and emits matching predicates. Two or more isolation columns → declare explicitly.

### 2. Access — Permission Checks

Role-based and record-based (deny by default). Reads (`GET /` and `GET /:id`) gate via `read.access`; mutations gate via `create.access`, `update.access`, `delete.access`, and `upsert.access`.

```typescript
read: { access: { roles: ['member'] } },  // gates GET / and GET /:id
create: { access: { roles: ['admin'] } },
update: {
  access: {
    or: [
      { roles: ['admin'] },
      { record: { createdBy: { equals: '$ctx.userId' } } },
    ],
  },
},
delete: { access: { roles: ['admin'] } },
```

> **DSL v2:** `crud.list` and `crud.get` are rejected at compile time. Move them to `read.access`.

Operators: `equals`, `notEquals`, `in`, `notIn`, `greaterThan`, `lessThan`
Context: `$ctx.userId`, `$ctx.activeOrgId`, `$ctx.roles`

### 3. Guards — Field Protection

```typescript
guards: {
  createable: ['name', 'description'],
  updatable: ['description'],
  immutable: ['invoiceNumber'],
  protected: { status: ['approve'] },  // Only via named actions
}
```

### 4. Masking — PII Redaction

```typescript
masking: {
  ssn: { type: 'ssn', show: { roles: ['admin'] } },
  email: { type: 'email', show: { roles: ['admin'], or: 'owner' } },
}
```

Types: `email`, `phone`, `ssn`, `creditCard`, `name`, `redact`, `custom`

## Views — Column Projections

```typescript
views: {
  summary: { fields: ['id', 'name'], access: { roles: ['member'] } },
  full: { fields: ['id', 'name', 'phone', 'ssn'], access: { roles: ['admin'] } },
}
```

## Validation

```typescript
validation: {
  name: { minLength: 1, maxLength: 100 },
  capacity: { min: 1, max: 1000 },
  roomType: { enum: ['meeting', 'conference'] },
  email: { email: true },
}
```

## Actions — Custom Business Logic

One file per action under `actions/<name>.ts`. The filename is the action name AND URL segment. `actions/<name>.ts` binds to the feature's primary table; `actions/<table>/<name>.ts` binds to a sibling table. Setting `path:` makes the action standalone.

```typescript
// features/todos/actions/complete.ts
import { z } from "zod";
import { eq } from "drizzle-orm";
import { defineAction } from "../.quickback/define-action";
import { todos } from "../todos";

export default defineAction({
  description: "Mark todo as complete",
  input: z.object({ completedAt: z.string().datetime().optional() }),
  access: { roles: ["member", "admin"], record: { completed: { equals: false } } },
  async execute({ db, record, auditFields }) {
    await db.update(todos)
      .set({ completed: true, modifiedAt: auditFields!.modifiedAt })
      .where(eq(todos.id, record.id));
    return { success: true };
  },
});
```

Record-based (no `path:`): `POST /api/v1/{resource}/:id/{action}`
Standalone (`path:` set): custom `path` and `method`

`execute:` must be an `async (ctx) => { … }` arrow function. Retired (rejected at load time): bundled `actions.ts`, `_feature.ts`, `handlers/` directory, `defineActions()`.

### Actions-Only Features (no tables)

A feature with no top-level `*.ts` files is "tableless" — every action must be standalone (have `path:`).

```typescript
// quickback/features/utilities/actions/health-check.ts
import { z } from "zod";
import { defineAction } from "../.quickback/define-action";

export default defineAction({
  description: "Health check",
  path: "/health",
  method: "GET",
  input: z.object({}),
  access: { roles: ["member", "admin"] },
  async execute() { return { ok: true }; },
});
```

### Public Actions

Use `roles: ["PUBLIC"]` for unauthenticated endpoints. Mandatory audit logged. The wildcard `"*"` is not supported.

```typescript
access: { roles: ["PUBLIC"] }  // No auth, audit logged
access: { roles: ["member"] }  // Requires auth + role
```

## Custom Dependencies

```typescript
export default defineConfig({
  build: {
    dependencies: {
      "fast-xml-parser": "^4.5.0",
    },
  },
});
```

## CMS Layouts

```typescript
layouts: {
  default: {
    sections: [
      { label: "Details", columns: 2, fields: ["name", "email"] },
      { label: "Notes", collapsed: true, fields: ["notes"] },
    ],
  },
}
```

## API Reference

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/v1/{resource}` | List |
| `GET` | `/api/v1/{resource}/:id` | Get |
| `POST` | `/api/v1/{resource}` | Create |
| `PATCH` | `/api/v1/{resource}/:id` | Update |
| `DELETE` | `/api/v1/{resource}/:id` | Delete |

Query params: `?limit=50&offset=0`, `?status=active`, `?sort=-createdAt`, `?fields=id,name`, `?search=text`
Filter operators: `.gt`, `.gte`, `.lt`, `.lte`, `.ne`, `.like`, `.in`

## Database Dialects

| Stack | Import | Table Function |
|-------|--------|----------------|
| Cloudflare D1 / SQLite | `drizzle-orm/sqlite-core` | `sqliteTable` |
| Supabase / PostgreSQL | `drizzle-orm/pg-core` | `pgTable` |

## Migrations are generated — never hand-merge them

The files under `quickback/drizzle/` (`*.sql`, `meta/_journal.json`, `meta/*_snapshot.json`) are **generated artifacts**, not source. Never hand-edit them, and never resolve a merge or rebase conflict inside them.

When integrating a branch: get the feature/schema `.ts` files in first, then run `quickback compile`. The compiler regenerates the migrations from the merged schema and journals them in the correct (append-only) order. Hand-merging migration SQL or snapshots produces schema drift and broken ordering that only surfaces at deploy time. If a migration file conflicts during a merge, take either side to unblock, then recompile and let the compiler restate the migrations.

## CLI Commands

```bash
quickback create <template> <name>   # Create project
quickback compile                     # Compile definitions
quickback docs [topic]                # Show documentation
quickback claude install              # Install Claude Code skill
quickback cursor install              # Install Cursor rules
quickback codex install               # Install Codex AGENTS.md rules
```

## Full Documentation

https://docs.quickback.dev
