# v0.18 and earlier

## v0.18.1 — May 18, 2026

### Account SPA — teams, active-team picker, permission-aware UI

The Account SPA's organization surface is now feature-parity with Better Auth's `organization` plugin for the basic team workflow, and replaces the long-standing hardcoded `role === 'owner' || 'admin'` checks with `checkRolePermission` so projects that ship custom roles get a UI that respects them.

#### Teams (opt-in)

When the BA config enables teams — `organization({ teams: { enabled: true } })` — the compiler emits `VITE_ENABLE_ORG_TEAMS=true` and the Account SPA exposes a Teams tab on each organization:

- **Teams index** (`/$slug/teams`) — splits into "Your teams" (from `listUserTeams`) and "Other teams" so non-admins land on what's relevant. Create / rename / delete from the list, gated on `team:create` / `team:update` / `team:delete`.
- **Team detail** (`/$slug/teams/$teamId`) — member roster with add / remove, joining `listTeamMembers` against the org's full member list for display.
- **Active-team picker** in the org header — calls `setActiveTeam` and exposes the choice via `OrganizationContext.activeTeamId` so downstream routes (and the BA `session.activeTeamId` server-side) stay in sync.
- **Team-aware invites** — `InviteMemberDialog` shows an optional team picker when teams are on and forwards `teamId` to `inviteMember`, so accepting the invite drops the new member straight into the right team.

Teams are loaded once by the `$slug` layout (`listTeams` + `listUserTeams`) and exposed via `OrganizationContext.teams` / `userTeamIds` / `refreshTeams`, so individual team routes don't re-fetch the list.

#### Permission-aware UI via `checkRolePermission`

`OrganizationContext.isOwnerOrAdmin` is gone. Routes now read `can(resource, action)` from context, which delegates to `authClient.organization.checkRolePermission` — synchronous, no roundtrip, and aware of any custom roles registered via the BA `ac` option. Multi-role users (BA stores multiple roles comma-separated) are handled by checking each role; the action is allowed if any role grants it.

Migrated sites:

- `$slug.tsx` tab gates → `can('invitation', 'create')`, `can('organization', 'update')`
- `members.tsx` action menu → `can('member', 'update' | 'delete')`; the owner-hierarchy guard (non-owners can't touch owner-role members) is preserved as a client-side mirror of BA's server enforcement
- `invitations.tsx` → `can('invitation', 'create' | 'cancel')` gates the Invite button, resend, and cancel separately
- `settings.tsx` → `can('organization', 'update')` for the form, `can('organization', 'delete')` for the danger zone (was owner-only)
- `teams.tsx` / `teams.$teamId.tsx` → `can('team', 'create' | 'update' | 'delete')`

Projects on the default three-role hierarchy see no behaviour change; projects that defined `admin`/`member` overrides via BA's access-control API now get a UI that matches what the server will actually authorize.

#### Editable slug + metadata on organization settings

The `$slug/settings.tsx` form now edits two more `organization.update` fields:

- **URL slug** — full debounced `checkSlug` flow (mirroring the create-org form); on rename, the SPA navigates to `/${newSlug}/settings` and the layout re-fetches with the new slug.
- **Metadata** — single `Record<string, any> | null` JSON textarea, parsed on submit with inline error messaging. Empty clears the field.

Slug rename is non-breaking for the SPA itself, but every external link / bookmark that references the old slug becomes a 404 — the form warns about this before saving.

#### Auth client wiring

When `VITE_ENABLE_ORG_TEAMS=true`, the SPA's `organizationClient(...)` is instantiated with `teams: { enabled: true }` so the team APIs are typed end-to-end. When the flag is off, team methods aren't surfaced — and the Teams tab is hidden — keeping the bundle and UI honest about what the server actually supports.

---

## v0.18.0 — May 17, 2026

### `config.apps` — mount user-authored SPAs on dedicated hostnames

The compiler now generates per-hostname Worker middleware for arbitrary user SPAs alongside the built-in CMS and Account UIs. Previously the only way to host a product app next to a Quickback API was a second Worker; `config.apps` brings the full stack — API, account, and product UI — onto one deployment with same-origin auth.

```ts
apps: {
  www: {
    domain: "www.example.com",
    aliasDomains: ["example.com"], // apex serves the same SPA
  },
}
```

Each entry binds one or more hostnames to a directory under `src/apps/<name>/` and inherits the Account-domain pass-through behavior — `/api/`, `/auth/`, `/admin/`, `/storage/`, and `/health` route to Hono so the SPA fetches its API without CORS. The compiler:

- emits a `custom_domain = true` route in `wrangler.toml` for the primary hostname and every alias
- adds each hostname to Better Auth's `TRUSTED_ORIGINS` (when `trustedOrigins` is left unset)
- widens the CSP `connect-src` to cover the SPA-baked origins
- folds the new hostnames into shared-parent detection for `crossSubDomainCookies`

Drop the prebuilt SPA bundle at `quickback/apps/<name>/` — the existing source-apps passthrough copies it byte-for-byte into `src/apps/<name>/`. The compiler doesn't build the SPA; it only emits routing for whatever's already there. The names `cms`, `account`, and `public` are reserved; hostname collisions with `cms.domain` / `account.domain` / `account.adminDomain` or between two `apps[*]` entries fail at compile time.

When `cms` and `account` are both configured, the multi-domain catchall now explicitly skips app hostnames — an API miss on an app host returns the standard 404 JSON via `app.notFound` instead of stray-serving a sibling SPA's HTML shell.

### Cross-subdomain cookie inference handles apex aliases

The shared-parent detection that drives `advanced.crossSubDomainCookies` previously reduced any hostname by stripping the leftmost label — `'example.com'` collapsed to `'com'`, which broke equality against sibling subdomains. Apex hostnames (exactly two dot-segments) are now treated as already being the parent, so apex aliases (`example.com` listed alongside `www.example.com`) coexist cleanly with `auth.example.com` / `cms.example.com` and the compiler still emits `crossSubDomainCookies: { enabled: true, domain: '.example.com' }`.

---

## v0.17.2 — May 15, 2026

### `customPlugins` escape hatch on `defineAuth("better-auth", …)`

The Better Auth provider now accepts `customPlugins` alongside the named-plugin registry — a path to ship third-party plugins (`@better-auth-kit/audit`, …), unregistered built-ins, or one-off project-local plugins without forking the compiler.

Each entry is an explicit import descriptor:

```ts
auth: defineAuth("better-auth", {
  emailAndPassword: { enabled: true },
  plugins: { passkey: true, organization: true },
  customPlugins: [
    // (1) Project-local plugin file at quickback/plugins/long-session.ts
    { from: "./plugins/long-session", export: "longSessionPlugin" },
    // (2) Third-party npm package
    {
      from: "@better-auth-kit/audit",
      export: "auditPlugin",
      args: { events: ["sign-in"] },
      dependencies: { "@better-auth-kit/audit": "^1.0.0" },
    },
  ],
}),
```

`from` is either a relative path under `./plugins/` (the CLI auto-uploads matching files from `quickback/plugins/`; the compiler copies them into `src/plugins/<name>.ts` and rewrites the emitted import) or a bare npm specifier (emitted verbatim; declare `dependencies` so the package lands in the generated `package.json`). Omit `args` to emit the symbol uncalled (plugin instance); provide `args` to emit a JSON-serialized factory call.

The descriptor is validated with Zod — typos in `from`/`export`, paths that escape `./plugins/`, and references to files the CLI didn't ship all fail at compile time with a targeted message.

---

## v0.17.1 — May 14, 2026

### `quickback start` — interactive onboarding

```bash
mkdir my-app && cd my-app
npx @quickback-dev/cli start
```

`start` walks new users through template selection, scaffolds the project on disk, and only prompts for an account when it's ready to compile. Cancelling at the login prompt leaves the scaffold intact — sign in and run `quickback compile` whenever you're ready.

The same deferred-auth flow applies to `quickback create`. Scaffolding and (optional) Cloudflare D1 setup run before authentication, so you can preview the project on disk before committing to an account.

### Named views on the free tier

The `todos` template now ships a `dashboard` named view, exposing a projected response at `GET /api/v1/todos/views/dashboard`. Named views support per-view access rules and per-view field selection while inheriting the table's firewall and masking — the canonical way to ship list-view payloads that are cheaper to fetch and easier to cache than a full collection read.

### `quickback init` removed

Folded into `start` and `create`. The legacy command name prints a deprecation notice pointing at its replacements.

### Minor

- `quickback whoami` reads the stored tier from your credentials.
- Pro upsell copy in `quickback login` is shown only when the server returns an explicit free-tier subscription.
- Bare scaffolds (`cloudflare`, `empty`) report accurately when there are no features to generate migrations for.

---

## v0.16.10 — May 14, 2026

### D1-specific safety pass on generated migrations

The `cloudflare-d1` provider now runs a post-generation pass over every Drizzle migration directory (auth, features, webhooks, audit) before the compile finishes. Better Auth cleanup for retired managed tables is stripped from raw `DROP TABLE` statements. Drop-only cleanup migrations are reordered child-first when foreign-key dependencies require it. Every generated D1 migration directory is replayed against SQLite during compile, so a D1-breaking migration fails at compile time rather than at `wrangler d1 migrations apply`.

---

## v0.16.9 — May 13, 2026

### Resource-scoped realtime tickets

`providers.database.config.realtime` accepts an object form for projects whose WebSocket subscribers aren't org members:

```ts
realtime: {
  wsTicket: {
    role: "attendee",
    requestField: "eventId",
    scopeTable: "events",
  },
}
```

The generated `/broadcast/v1/ws-ticket` route authenticates the caller, evaluates the declared relationship role against the request, and mints a short-lived WebSocket ticket scoped to the resolved resource. `realtime: true` continues to enable the org-scoped broadcaster.

### Unified broadcast targeting

The realtime helper and Broadcaster now route by `scopeKey || organizationId || userId`. String targets remain shorthand for `organizationId`; explicit targets can scope a broadcast to a resource or a single user:

```ts
await realtime.broadcast("event-page-updated", payload, { scopeKey: "events:evt_123" });
await realtime.broadcast("dm", payload, { userId: recipientId });
```

WebSocket tickets are Quickback-minted (signed with `BETTER_AUTH_SECRET`), not Better Auth tokens — Better Auth remains the front-door auth check; Quickback signs the upgrade ticket after authorization succeeds.

---

## v0.16.8 — May 13, 2026

### One auth transport per request

Generated middleware treats `Cookie`, `Authorization: Bearer`, and `x-api-key` as distinct auth methods and rejects mixed requests with `400 AUTH_METHOD_CONFLICT`. Query-string API keys (`?api_key=` / `?key=`) are no longer accepted. Machine-to-machine auth is explicit; precedence rules between transports are gone.

### Cookie-backed CSRF gate

Generated apps now block cross-site `POST` / `PUT` / `PATCH` / `DELETE` requests from cookie-relying callers. The middleware skips explicit `Authorization` clients, requires a trusted `Origin` or `Referer`, and blocks `Sec-Fetch-Site: cross-site`. This matters most for cross-subdomain session setups where `SameSite=None` is intentional.

### First-class Wrangler observability

`providers.runtime.config.observability` now supports nested `logs` / `traces` blocks for `headSamplingRate`, `destinations`, `persist`, and `logs.invocationLogs`, emitting matching `[observability]`, `[observability.logs]`, and `[observability.traces]` TOML.

---

## v0.16.7 — May 12, 2026

### Bearer wins over cookie

A request carrying both `Authorization` and a session `Cookie` resolves entirely from the bearer; the cookie is dropped before `getSession()` runs. All three Bearer flavors (API key, JWT, session-token) now behave consistently — no more silent cookie precedence on session-token bearers.

### Bearer plugin defaults to `requireSignature: true`

Better Auth's `bearer` plugin defaults to unsigned tokens; Quickback now emits `bearer({ requireSignature: true })`. Tokens emitted in the CORS-exposed `set-auth-token` header are server-signed with `BETTER_AUTH_SECRET` and verified on every request. Projects that genuinely need unsigned bearers can override via `providers.auth.config.plugins.bearer`.

See [Authentication Security → Bearer Tokens and Cookie Precedence](/platform/auth/security#bearer-tokens-and-cookie-precedence).

---

## v0.16.5 — May 12, 2026

### Flat write DSL

Resource write operations are now declared at the table level alongside `read`, replacing the nested `crud` block:

```ts
defineTable(customers, {
  read: { ... },
  create: { access: { roles: ["admin"] } },
  update: { access: { roles: ["admin"] } },
  delete: { access: { roles: ["admin"] }, mode: "soft" },
  upsert: { access: { roles: ["admin"] } },
  guards: { ... },
});
```

`crud` and top-level `put` still compile as deprecated compatibility aliases. Mixed usage on the same resource is rejected. `schema-registry.json` emits both shapes during the transition.

### CMS placement for multi-table feature actions

Standalone actions on multi-table features have an explicit placement model:

- `cms.placement: "feature"` shows the action on every table toolbar in the feature
- `cms.placement: "tables"` with `cms.tables` scopes it to specific tables
- `cms.hidden: true` keeps it API-only

Feature-root standalone actions are emitted under a new `featureActions` bucket in `schema-registry.json`. Ambiguous multi-table standalone actions without placement compile with a warning.

### `openapi.types` accepts a string array

One compile can write the generated types file to multiple locations, keeping colocated SPA clients in sync:

```ts
openapi: { types: ['api-types.gen.ts', 'apps/web/src/lib/api-types.gen.ts'] }
```

### Stable artifact ordering

Generated route files and `schema-registry.json` are emitted in a stable order across runs. Identical logical input produces identical artifacts.

---

## v0.16.4 — May 12, 2026

### Drizzle snapshot integrity

Drizzle metadata files (`<n>_snapshot.json`) are no longer stamped with a Quickback warning key. The warning is now journal-only. Projects whose snapshots were corrupted by an earlier release self-heal on the next compile — the stripped header is removed during staging and the rewritten snapshot is clean.

### `account.auth.*` drives the server, not just the SPA

The Account UI's auth flags (`signup`, `password`, `passkey`, `emailOTP`, `emailVerification`) now configure the Better Auth server alongside the SPA bundle. A normalizer maps them into the server config:

- `signup → emailAndPassword.disableSignUp` (inverted)
- `password → emailAndPassword.enabled`
- `emailVerification → emailAndPassword.requireEmailVerification`
- `passkey` / `emailOTP` → plugin additions

The SPA's `VITE_ENABLE_*` flags are derived from the same canonicalized server config, so the bundle and the server cannot drift. Two new flags ride along: `VITE_ENABLE_API_KEYS` and `VITE_ENABLE_MAGIC_LINK`.

**Breaking** for projects that implicitly relied on the SPA-only behaviour: `account.auth.signup: false` now actually disables `POST /auth/v1/sign-up/email` on the server. Set `providers.auth.config.emailAndPassword.disableSignUp: false` explicitly to keep server signup open while hiding the SPA screen.

---

## v0.16.3 — May 9, 2026

### Standalone action mount order

Feature-level standalone action routers now register before sibling CRUD routers when both mount at the exact same prefix. A literal path like `GET /api/v1/records/today-bundle` resolves to the standalone handler instead of falling through to `GET /:id` and treating `"today-bundle"` as a record ID.

---

## v0.16.1 — May 9, 2026

### Cloudflare ASSETS canonicalization

Worker SPA fallbacks now request `/<dir>/` instead of `/<dir>/index.html`. Cloudflare's static-assets binding rewrites `/index.html` requests as 307 redirects to the trailing-slash path under the default `html_handling=auto-trailing-slash`, which previously caused redirect loops in SPA fallback handlers. Affects CMS, Account, admin, and the multi-domain catchall.

### `account.customLogin`

Set `account: { customLogin: true }` to drop a project-branded sign-in page in front of the bundled SPA. Place the page at `quickback/public/account/login/index.html` (as a directory) and the existing public passthrough serves it. The route registers before the `/account/*` SPA catch-all so the static handler wins; everything else under `/account/` continues to route through the bundled SPA.

### Project-level realtime defaults

`providers.database.config.realtime: true` now treats every resource as realtime-enabled by default. Per-resource `realtime: { enabled: false }` still wins.

### Compile-time validators

Two new compile-time errors over silent runtime failures:

- `AUTHENTICATED` + org-scoped firewall is rejected. The combination silently returned empty lists to sign-in-only callers who lacked an active org; the validator names three valid resolutions (drop the org scope, use `USER`, or split the route).
- `createRealtime` imports without `realtime: true` enabled in the config are rejected at compile.

### Drizzle metadata hardening

Drizzle journal files carry a "do not edit" header. The compiler warns when the journal entry count exceeds the snapshot file count (typically the trace of a hand-deleted snapshot).

### Minor

- `SYSADMIN` is now recognised by the standalone-action org gate's pseudo-role matcher.
- Apex `/` and `/account/login` get fallback redirects to `auth.loginUrl` when neither bundled SPA is enabled.
- Record-based action URLs are kebab-case to match the OpenAPI spec (e.g. `/api/v1/widgets/:id/send-invite`).

---

## v0.16.0 — May 9, 2026

### Agent Auth Protocol

Quickback ships a server implementation of the [Agent Auth Protocol](https://agentauthprotocol.com) via `@better-auth/agent-auth`. Enable with `auth.plugins.agentAuth: true` and the compiler emits the full agent-auth surface: discovery at `/.well-known/agent-configuration`, capability projection from the project's OpenAPI spec, a device-authorization approval page in the Account SPA at `/agents/approve`, and the four agent-auth tables added to the auth schema automatically.

**OpenAPI capability projection runs by default.** Every operation with an `operationId` becomes an agent capability. Default `approvalStrength` map: `GET → "session"`, `POST/PUT/PATCH/DELETE → "webauthn"`. The proxy `onExecute` makes a self-fetch with a Better Auth JWT minted for the agent session, so the inner request hits the existing JWT fast-path and runs the full firewall + access + masking pipeline. Opt out of OpenAPI projection with `agentAuth: { fromOpenAPI: false }` and wire `capabilities` + `onExecute` by hand.

### Bearer-only on `/mcp` and the agent-auth surface

`/mcp` forwards only `Authorization: Bearer …` into the inner request. Cookie and `x-api-key` forwarding are removed. Three Bearer flavors land on the same header — Better Auth session token (via the `bearer` plugin), OAuth access token (via OAuth Provider), and agent JWT (via `@better-auth/agent-auth`).

### Auth DSL gains first-class plugin entries

`bearer`, `oauthProvider`, and `agentAuth` are now typed entries in both the array-form union and the `BetterAuthPlugins` interface, with their own options shapes (`OAuthProviderOptions`, `AgentAuthOptions`). Existing `as any` casts can come out.

Auto-enable rule: turning on `agentAuth` force-adds `bearer` + `oauthProvider`. No useful agent-auth deployment ships without those two.

### `queue-consumer.ts` gating

The queue consumer is emitted when any of the following are true: embeddings are configured, queue handlers exist under `services/queues/*`, or `additionalQueues` is declared. Previously the third case left projects with a wrangler binding pointing at a missing handler.

---

## v0.15.0 — May 7, 2026

### Sysadmin tier for the CMS

A new opt-in `cms: { sysadmin: true }` flag turns the CMS into a cross-tenant DB-admin tool gated by `user.role === 'sysadmin'`. Distinct from the Better Auth `'admin'` plugin role, so user-management powers and DB-admin powers stay decoupled. Off by default.

When enabled, the compiler emits a `SYSADMIN` pseudo-role (a fifth marker alongside `PUBLIC` / `AUTHENTICATED` / `USER` / `ADMIN`), a firewall escape hatch that drops tenant scopes for sysadmins while preserving soft-delete, a true cross-tenant `/admin/v1/sysadmin/organizations` endpoint, and a relaxed cross-tenant guard for list routes.

The CMS SPA gains a two-mode design: org admins keep the existing membership-based switcher, sysadmins get an "All organizations" selector that pins tenants via `?organizationId=` on every API call.

### Durable Object `[[migrations]]` order is preserved

Cloudflare's migration list is append-only. The compiler now reads your existing `wrangler.toml` (when present) and emits auto-generated DO `[[migrations]]` blocks in two phases: deployed entries first (preserving order), net-new entries appended last. Reordering DOs in the config array no longer reshuffles deployed migrations.

### `quickback/public/` — static-asset passthrough

Anything dropped under `quickback/public/` is copied verbatim into `src/apps/` on every compile and served by the wrangler `[assets]` binding. Use for favicons, `robots.txt`, OG images, `.well-known/` files, marketing PDFs — anything that needs to ride past the compiler as bytes.

- Optional. Missing folder is a no-op.
- Stale-cleaned. Files removed from `quickback/public/` disappear on the next compile.
- Hidden entries (`.DS_Store`, dotfiles) are skipped.
- Collisions with compiler-emitted manifest paths fail the compile loudly.

### Minor

- Standalone-action wrapper skips the org gate when the access tree references any pseudo-role.
- Queue handler emit no longer drops the `queue` export when there are no embeddings.

---

## v0.14.7 — May 7, 2026

### Codegen fixes for projects with non-`id` primary keys and DO-only features

Four surgical fixes to the generated output:

- Duplicate table imports in relationship-role routes are deduplicated.
- `db` references in single-record and bulk action handlers now resolve correctly during pre-fetch.
- PartyServer auth gates pass the in-scope `db` handle into firewall calls.
- Durable-object-only features keep their `lib/` files in the output.

### `emailOtp` + AWS SES is OTP-only

`auth.emailOtp` with the AWS SES email provider no longer auto-wires the magic-link companion. Mail-client URL prefetching can burn the one-time code before the human sees it; the Account SPA's `/email-otp` route is the single canonical entry point. Projects that explicitly want the magic-link arm can wire `sendVerificationOTP` directly.

---

## v0.14.6 — May 7, 2026

### MCP transport correctness

Four fixes to the MCP-over-HTTP path:

- `tools/call` URLs are no longer double-prefixed for action tools.
- Auth middleware accepts API keys via `Authorization: Bearer` in addition to `x-api-key` and query parameters — the channel MCP clients use exclusively.
- `auth.roleHierarchy` is expanded for runtime role checks across api-key, JWT, and session auth paths. A higher role inherits the privileges of every lower role.
- Team-scoped firewall predicates degrade gracefully when no team is active (org scoping continues to apply; team scoping is optional).

---

## v0.14.5 — May 7, 2026

### MCP environment threading

The generated MCP server's `tools/call` dispatch threads `env` and `executionCtx` into the inner request, so action tools that touch Cloudflare bindings (D1, KV, R2, Queues) no longer crash with `undefined` env access.

---

## v0.14.4 — May 7, 2026

### `via:` firewall predicates pull required imports

The relationship-role generator emits `inArray` and `sql` imports when the firewall has any `via:` predicate, and `buildFirewallConditions` accepts `(ctx, db)` so the relationship subquery has a Drizzle client to evaluate against. Every CRUD and action call site passes `db` through automatically.

---

## v0.14.3 — May 7, 2026

### Relationship-role table re-exports

Generated `<table>.resource.ts` files now re-export their Drizzle table binding. Cross-feature relationship subqueries that import the binding from the resource file (rather than the schema) resolve correctly.

---

## v0.14.2 — May 7, 2026

### Historical DO `[[migrations]]` are preserved

A new `bindings.durableObjectMigrations` config slot carries forward `deleted_classes`, `renamed_classes`, and `transferred_classes` lifecycle entries across recompiles. Cloudflare's migration list is append-only; entries declared here are emitted first in `wrangler.toml`, then auto-generated current-class entries follow:

```ts
bindings: {
  durableObjects: [{ name: 'NOTEPAD', className: 'Notepad', from: 'lib/Notepad' }],
  durableObjectMigrations: [
    { tag: 'qb-do-deleted-legacy-notes-2026-05-05', deletedClasses: ['LegacyNotes'] },
  ],
}
```

All four lifecycle shapes are supported. User-declared entries can override auto-emit by reusing the same tag.

---

## v0.14.1 — May 7, 2026

### Bulk action variants in `openapi.json`

Record-based actions with `bulkVariant: true` are now emitted as sibling operations in the OpenAPI spec, with the documented `{ ids, input, failFast? }` request shape, 200/207/400 responses, and a separate `<Resource><Action>Bulk` `operationId` so typed-client codegen distinguishes single from bulk.

### Cross-tenant "my events" patterns

The access docs now include a worked example for the cross-tenant aggregation pattern (e.g. `GET /my-events` returning resources across all tenants a caller is invited to). See [Access → Cross-tenant dashboards](/define/relationships#cross-tenant-my-events-dashboards-are-not-this).

---

## v0.14.0 — May 7, 2026

### Relationship-based authorization

Roles can now be bound to a domain-table membership ("user is a confirmed guest of this event") rather than an org membership or a pseudo-role. Declare the relationship once and reference it from `firewall:` or `access.roles:`:

```ts
authz: {
  relationships: {
    guestOf: {
      from: 'event_guests',
      subject: { column: 'userId', equals: 'ctx.userId' },
      resource: { column: 'eventId' },
      where: { status: 'confirmed' },
    },
  },
  roles: {
    guest: { via: 'guestOf' },
    eventStaff: { or: [{ via: 'guestOf' }, { roles: ['admin', 'owner'] }] },
  },
}

// features/sessions/sessions.ts
firewall: [
  { field: 'organizationId', equals: 'ctx.activeOrgId' },
  { field: 'eventId', via: 'guestOf' },
],
read:   { access: { roles: ['admin', 'member', 'guest'] } },
create: { access: { roles: ['admin', 'guest'] } },
update: { access: { roles: ['admin'] } },
```

The compiler emits an inline Drizzle subquery against the relationship table and AND-composes it as the outermost firewall clause. The relationship table's own firewall (tenant scope, soft-delete) is honored inside the subquery; cross-tenant grants are impossible because the resource's tenant filter sits outermost.

Supported surfaces: row-level scoping via `firewall: [{ field, via }]`, role gates on writes and reads, view access. Masking and action access reject relationship-roles at compile time with pointers to the working pattern (gate the row at the firewall layer, gate the role at the access layer).

Cross-tenant aggregation dashboards aren't solved by relationship-roles — the tenant firewall remains the outermost AND. For that pattern, use a user-scoped firewall or function-form access on a dedicated route.

See [Firewall → Relationship-based scoping](/define/firewall#relationship-based-scoping-via) and [Access → Relationship-based authorization](/define/relationships).

### Bulk variants on record-based actions

`defineAction({ bulkVariant: true })` emits `POST /:resource/batch/{action}` alongside the per-record route. The bulk handler loops the same access + transition + execute pipeline over a `{ ids, input, failFast? }` body:

```ts title="features/applications/actions/advance.ts"
import { z } from "zod";
import { defineAction } from "../.quickback/define-action";

export default defineAction({
  description: "Move an application forward in the pipeline.",
  input: z.object({ nextStatus: z.enum(["screening", "interview", "offer"]) }),
  access: { roles: ["owner", "admin"] },
  bulkVariant: true,
  transition: { field: "status", via: "nextStatus", fromTo: { /* ... */ } },
  async execute({ db, record, input, whereTransition }) {
    // exact same body — runs once per id
  },
});
```

Per-record outcomes return as 207 Multi-Status. `failFast: true` aborts on the first failure and wraps the loop in `db.transaction()` on tx-supporting providers; D1 fails fast without rollback. Max 100 IDs per request. Rejected on standalone actions and `unsafe` actions.

### Aggregations on read views

Read views can declare pre-computed metrics that surface alongside the row payload:

```ts
read: {
  views: {
    pipeline: {
      fields: ["id", "jobId", "candidateId", "status", "appliedAt"],
      aggregations: {
        totalApplications: { fn: "count" },
        byStatus:          { fn: "groupBy", field: "status" },
      },
    },
  },
}
```

Aggregations run over the same firewalled, filtered set as the row query; pagination is intentionally ignored. Supported functions: `sum`, `avg`, `min`, `max`, `count`, `count_distinct`, `groupBy`. See [Views → Aggregations](/define/views#aggregations).

---

## v0.13.5 — May 6, 2026

### Object-form `auth.plugins` config is honored

The documented `plugins: { oauthProvider: {...} }`, `plugins: { organization: { teams: { enabled: true } } }` shapes now flow through the compiler unchanged. Per-plugin options reach the generated `auth.ts` as written.

### `roles: ["PUBLIC"]` allowed on writes and actions

`PUBLIC` is the explicit, uppercase opt-in for unauthenticated access on every access node — reads, writes, views, and actions. The runtime trusts the marker; row-level safety comes from firewall, masking, and the action's own validation. `PUBLIC` actions continue to receive mandatory audit logging.

Common pattern: pair `roles: ["PUBLIC"]` with `firewall: { exception: true }` on the resource so the row predicate doesn't reject every unauthenticated caller.

---

## v0.12.0 — May 5, 2026

### Per-room authorization for PartyServer

`bindings.durableObjects[].access` declares a path-scoped gate that runs the declared resource's firewall and `read.access` pipeline before the WebSocket upgrade:

```ts
durableObjects: [
  // Single-resource: roomId IS the chat row id.
  { name: 'Chat', className: 'Chat', from: 'lib/Chat',
    access: { resource: 'chats' } },

  // Multi-resource: regex dispatches kind → resource.
  { name: 'Notepad', className: 'Notepad', from: 'lib/Notepad',
    access: {
      roomId: { pattern: '^(day-note|interaction|record):([A-Za-z0-9_-]+)$', kindGroup: 1, idGroup: 2 },
      resources: {
        'day-note':    { resource: 'day-notes' },
        'interaction': { resource: 'interactions' },
        'record':      { resource: 'records' },
      },
    } },

  // Anonymous lobbies / public pads: explicit opt-out.
  { name: 'PublicLobby', className: 'PublicLobby', from: 'lib/PublicLobby',
    access: 'public' },
]
```

The gate 401s unauthenticated callers, 403s when the firewall hides the row, 404s when the row is missing. Resource references are validated against feature names at compile time. See [PartyServer Rooms → Per-room authorization](/platform/realtime/partyserver-rooms#per-room-authorization-via-access-v012).

---

## v0.11.1 — May 5, 2026

### PartyServer auto-mount is secure by default

The `/realtime/*` auto-mount now wraps `partyserverMiddleware` in an auth check. Unauthenticated upgrades return 401; the four auth mechanisms (session cookie, JWT bearer, `x-api-key` header, query parameter) all unlock connections. Per-room access remains the `Server` class's responsibility in `onConnect`.

---

## v0.11.0 — May 5, 2026

### OAuth Provider (MCP-ready)

Quickback can act as a standards-compliant OAuth 2.1 + OIDC authorization server via [`@better-auth/oauth-provider`](https://better-auth.com/docs/plugins/oauth-provider). Enable with `plugins: ["oauthProvider"]`. The compiler auto-includes Better Auth's `jwt` plugin and the Account SPA gains a `/consent` page for the standard authorization-code flow.

This replaces Better Auth's deprecated `mcp` plugin and gives every MCP client (Cursor, Claude Desktop, claude.ai connectors, custom tooling) RFC 7591 dynamic client registration plus the discovery documents they need to bootstrap. See [Auth Plugins → OAuth Provider](/platform/auth/plugins#oauth-provider-mcp-ready).

### API keys via query parameter

The auth middleware now accepts API keys as `?api_key=…` or `?key=…` query parameters. The `x-api-key` header takes precedence when both are sent. Prefer the header whenever the client supports it — query parameters land in access logs and `Referer` headers.

### PartyServer rooms + realtime URL split

The realtime URL space is reorganized so names match what each primitive does:

- **`/realtime/v1/*` → `/broadcast/v1/*`** — the org-wide Broadcaster moves to its semantically accurate prefix.
- **`/realtime/<binding>/<room-id>`** — new mount for [PartyServer](/platform/realtime/partyserver-rooms) rooms. Per-room Durable Objects with presence and lifecycle hooks. Auto-mounted whenever a project declares any in-worker DO.

**Breaking** — recompile + redeploy required:

- `REALTIME_URL` env var renamed to `BROADCAST_URL`.
- SPA helpers `getRealtimeWsUrl()` / `getRealtimeTicketUrl()` renamed to `getBroadcastWsUrl()` / `getBroadcastTicketUrl()`.
- `appConfig.routes.api.realtime` renamed to `appConfig.routes.api.broadcast`.

`partyserver`, `partysocket`, and `hono-party` are pre-installed in the compiler image; collab projects compile without an extra `npm install` step.

---

## v0.10.20 — May 4, 2026

### In-worker Durable Objects

`bindings.durableObjects[].from` flips a DO binding into in-worker mode and emits the export, the `[[migrations]]` block, and the `CloudflareBindings` type all together:

```ts
durableObjects: [{
  name: "ITEM_STREAM",
  className: "ItemStream",
  from: "features/loops/lib/ItemStream",
}]
```

DO class files live under `quickback/lib/` or `quickback/features/<feature>/lib/` — both copy verbatim into the generated `src/` tree on every compile. `from` and `scriptName` are mutually exclusive; cross-worker DOs (`scriptName` set) skip the re-export and migration block.

`migrationTag` (default `qb-do-${className}`) is stable across regenerations and must never be removed or renamed once deployed. `useSqlite` defaults to `true`. See [Bindings → Durable Objects](/configure/bindings#durableobjects).

---

## v0.10.18 — May 2, 2026

### `experimentalRemote` — share deployed D1 with `wrangler dev`

Per-binding `experimental_remote = true` lets `wrangler dev` read/write deployed D1 for selected bindings:

```ts
providers: {
  database: defineDatabase("cloudflare-d1", {
    splitDatabases: true,
    experimentalRemote: { auth: true, features: false },
  }),
}
```

Useful for "share prod auth in dev, keep feature writes sandboxed." Off by default.

### Google OAuth + signup ToS

The Account SPA wires Google OAuth end-to-end when configured under `auth.config.socialProviders.google.enabled`. The signup flow gains a Terms-of-Service checkbox gate (configurable via the Account SPA branding).

### CLI compile auth gate

`quickback compile` now requires authentication for cloud-compiler runs. Device-flow `?redirect=` parameters are standardized across the login + signup pages so a user clicking "Create one" no longer loses their return URL.

---

## v0.10.15–v0.10.17 — May 2, 2026

### CSP auto-config

When `security.csp` is set, the compiler widens directives additively based on the project's enabled features:

| Trigger | Auto-added | Directive |
|---|---|---|
| `cms: true` OR `account: true` | `'unsafe-inline'`, `https://fonts.googleapis.com` | `styleSrc` |
| `cms: true` OR `account: true` | `https://fonts.gstatic.com` | `fontSrc` |
| `csp` set (always) | `@trustedOrigins` | `connectSrc` |
| Bundled SPA enabled | SPA-baked API origin (`config.domain`, `quickback.{baseDomain}`, `cms.domain`, `account.domain`) | `connectSrc` |

Anything you already wrote stays — auto-config never removes or overrides. Each applied addition is logged to stderr. Opt out with `security: { cspAutoConfig: false }`.

Most projects can write the minimal:

```ts
security: {
  csp: {
    defaultSrc: ['@self'],
    scriptSrc: ['@self'],
    objectSrc: ['@none'],
    upgradeInsecureRequests: true,
  },
}
```

…and the compiler fills in font hosts, trusted origins, and SPA-baked URLs.

When a target directive is missing, auto-config scaffolds it from `default-src` baseline rather than widening `default-src` itself — CSP3 fall-back semantics would otherwise leak the addition to every directive that falls back to `default-src`.

---

## v0.10.14 — May 1, 2026

### `auth.jwt` config

The bearer-token JWT used by the auth fast-path is configurable. Tune `expiresIn` to bound the post-revocation replay window without affecting browser SPAs (which auto-refresh on every authed call):

```ts
auth: {
  jwt: {
    expiresIn: 30,
    issuer: 'my-api',
    audience: 'partner-svc',
    secretEnv: 'CUSTOM_JWT_SECRET',
  },
}
```

### Admin MCP endpoints filtered

Better Auth admin endpoints (`/admin/list-users`, `/admin/ban-user`, etc.) now carry `x-access: { userRole: ['admin'] }` in the generated OpenAPI spec. The MCP `tools/list` access-filter drops them for non-admin callers.

### Declarative `security` config

Every security response header (`Content-Security-Policy`, COOP, COEP, CORP, HSTS, `X-Frame-Options`, `Referrer-Policy`, `Permissions-Policy`) is configurable from `quickback.config.ts`. `@`-prefixed sentinels (`@self`, `@none`, `@unsafe-inline`, `@trustedOrigins`, etc.) replace awkward `"'self'"` quoting.

The compiler lints the resolved config for known footguns: `@unsafe-inline` in `script-src`, wildcard sources, missing `default-src`, HSTS preload-list misconfiguration.

### HSTS skipped on localhost

Generated middleware skips `Strict-Transport-Security` when the request hostname is `localhost`, `127.0.0.1`, or `*.localhost`. Without this gate, `wrangler dev` would pin a developer's browser to HTTPS across every project on that machine.

---

## v0.10.13 — May 1, 2026

### Clickjacking defence on every Worker

Every Worker emits `Content-Security-Policy: frame-ancestors 'self' <TRUSTED_ORIGINS…>` and `X-Frame-Options: SAMEORIGIN` on every response. The CSP allowlist is sourced from the same `TRUSTED_ORIGINS` constant that gates CORS.

---

## v0.10.11 — May 1, 2026

### MCP `tools/list` is access-filtered

Each MCP tool now carries sanitised access metadata. `tools/list` filters against the caller's `ctx`: unauthenticated callers see only `PUBLIC` tools, authenticated callers see only role-gated tools they pass.

- Wildcard CORS removed from the MCP endpoint.
- Unauthenticated `GET /mcp` no longer enumerates tool names.
- Internal flavor text is removed; `annotations.destructiveHint` and `_unsafe` remain — those are the spec-correct signals for clients.

`tools/call` continues to re-enter the Hono app and run every middleware (auth, firewall, access, guards, masking), so hiding a tool from list never weakens the underlying gate.

---

## v0.10.10 — April 29, 2026

### Reserved keys in `access.record` are rejected

`access.record` is a flat field map evaluated as implicit AND between entries — keys must be column names. The compiler now rejects reserved keys (`and`, `or`, `roles`, `userRole`) inside `record` with migration guidance pointing at the working pattern (top-level siblings with their own `record:` blocks).

---

## v0.10.8 — April 29, 2026

### Audit logging on Postgres targets

`audit_events` previously only existed on `cloudflare-d1`. The audit table now lives in a dedicated `audit` schema (`drizzle.pgSchema('audit')`) on Postgres targets. The `hasSecurityAudit` gate accepts D1 or any PG dialect; `auditDatabaseId` remains a D1-only requirement.

### `auditDatabaseId` is a compile-time requirement

Projects with unsafe or `PUBLIC` actions that previously produced a placeholder `database_id = "local-audit"` in production now fail at compile time with the exact `wrangler d1 create <project>-audit` command and the config key where the returned ID goes.

### Action input → OpenAPI / MCP

`defineAction({ input: z.object({...}) })` schemas flow into the OpenAPI `requestBody` and the MCP tool's `inputSchema.properties`. Simple `z.object` shapes are extracted at compile time; discriminated unions, refinements, recursive schemas, and bare-identifier references run through Zod's native `z.toJSONSchema()` for full-fidelity output.

### OpenAPI fidelity

- Response schemas emit `required[]` for every non-nullable column.
- `text(_, { enum: [...] })` columns emit as closed `{ type: "string", enum: [...] }`.
- `id` claims `format: "uuid"` only when the project's `generateId` actually produces UUIDs. cuid2 / prefixed / nanoid / serial generators emit plain `{ type: "string" }`.

### Minor

- Auto-mask predicate verifies the resolved owner column exists before emitting an `or: 'owner'` clause; falls back to roles-only with a warning otherwise.
- Generated CRUD errors emit canonical `QuickbackErrorCode` enum values (`DB_NOT_NULL_VIOLATION`, `DB_UNIQUE_VIOLATION`, etc.) so generated SDKs can exhaustively switch on `code`. See [API errors](/api/errors#database).

---

## v0.10.5 — April 28, 2026

### One file per action

Action authoring is one file per action under `<feature>/actions/<name>.ts`. The filename is the action name and the URL segment:

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

export default defineAction({
  description: "Move an application forward in the pipeline",
  input: z.object({ nextStatus: z.enum(["screening", "interview", "offer"]) }),
  access: { roles: ["owner", "admin"] },
  transition: {
    field: "status",
    via: "nextStatus",
    fromTo: { applied: ["screening"], screening: ["interview"], interview: ["offer"] },
  },
  async execute({ db, record, input, auditFields, whereTransition }) { /* … */ },
});
```

`defineAction` is imported from a generated per-feature helper at `<feature>/.quickback/define-action.ts` — `record`, `input`, and audit fields are typed without manual annotations.

**Discovery rules:**
- `actions/<name>.ts` binds to the feature's primary table.
- `actions/<table>/<name>.ts` binds to the sibling table file.
- Setting `path:` makes the action standalone (custom URL, no record fetch).
- Tableless features require every action to be standalone.

**Retired** (rejected at load time with migration guidance): `actions.ts` (bundled multi-action file), `_feature.ts`, `handlers/` directory + `handler:` string ref, `defineActions(table, {…})`, colocated `defineActions` inside table files, `standalone: true` flag, `*-actions.ts` / `*.actions.ts` filename patterns.

See [Actions docs](/define/actions).

### Multi-table actions per feature

One feature directory can own actions on multiple tables, through the per-file discovery rules above: `actions/<name>.ts` binds to the feature's primary table, `actions/<table>/<name>.ts` binds to the sibling table file of that name.

Action names are unique per `(bindTable, name)` pair. Sibling-bound `actions/episodes/publish.ts` and `actions/shows/publish.ts` mount at `/episodes/:id/publish` and `/shows/:id/publish` and compile cleanly.

> **Correction.** As originally published, this entry listed three discovery modes — colocated `defineActions(<table>, …)` inside the table file, a bundled `actions.ts`, and `actions/<table>.ts` — plus a universal `_feature.ts` slot. All four were retired by the one-file-per-action change in this same release (see the entry above) and are rejected at load time today. The shape stated here is the surviving one.

### Firewall: named-scope shape

The named-scope firewall shape is a co-equal user-facing input alongside the predicate array:

```typescript
firewall: { organization: { column: 'tenant_id' } }     // named-scope (compact)
firewall: { owner: { column: 'account_user_id' } }
firewall: { exception: true }
firewall: [
  { field: 'organizationId', equals: 'ctx.activeOrgId' },
  { field: 'deletedAt', isNull: true },
]                                                       // predicate array (explicit)
```

Pick the form that reads better — named-scope for the common one-line case, predicate array when you need custom `in`-lists or non-scope predicates.

**Auto-detection synonyms.** When `firewall:` is omitted, the compiler scans the schema for an isolation column. The org-scope list accepts `organizationId`, `organisationId`, `orgId`, `organization`, `organisation`, `org` (American + British) — the first match in the schema source becomes the predicate field. Owner scope accepts `userId` **in this release**.

**`ownerId` is a targeted error signal.** When a table carries `ownerId` but no isolation column and no explicit `firewall:`, the compiler raises a specific error — `ownerId` is business-logic ownership, not access control. Use `userId` for access control, declare an explicit `firewall:`, or add a separate isolation column.

> **Superseded in v0.21.0 — do not follow the two paragraphs above.** The owner
> convention was inverted: `ownerId` is the auto-detected owner column (the
> compiler firewalls reads with `WHERE ownerId = ctx.userId` and auto-stamps the
> column on insert) and `userId` is **not** auto-detected on feature tables — it
> is a Better Auth convention on Better Auth's own tables (`session`, `account`,
> `member`, `passkey`), and on a feature table it usually means *a user this row
> references* (member, contact, invitee) rather than *the row's owner*. If
> `userId` is the only candidate column the compiler raises an error; alongside a
> detected isolation column it raises a warning. Two escapes: rename the column to
> `ownerId`, or declare `firewall: { owner: { column: "userId" } }` explicitly.
> The two paragraphs above describe v0.10.5 behaviour only — see
> *Auto-firewall column swap* under v0.21.0.

---

## v0.10.0 — April 26, 2026

### `q.scope()` — single-declaration tenant scoping

`q.scope(kind)` replaces the three-place pattern (column declaration + firewall predicate + ownership-guard) with one column:

```typescript
columns: {
  organizationId: q.scope('organization'),  // → ctx.activeOrgId
  ownerId:        q.scope('owner'),         // → ctx.userId
  teamId:         q.scope('team'),          // → ctx.activeTeamId
}
```

`q.scope()` columns auto-derive the firewall predicate, register with the guards layer as `systemManaged` (client input rejected), and auto-populate from `ctx` on create / upsert. The systemManaged extension generalises beyond `q.scope()`: any firewall predicate whose `equals` references `ctx.*` registers its `field` automatically.

See [`q.scope()`](/define/schema#qscope--tenant-scope-columns).

### `masking[col].query.roles` — unified filter / sort / search gate

The three legacy per-capability flags (`filterable`, `searchable`, `sortable`) are replaced by a single `query.roles` allowlist:

```typescript
masking: {
  email: {
    type: 'email',
    show:  { roles: ['admin'] },
    query: { roles: ['admin'] },  // defaults to show.roles when omitted
  },
}
```

A view's own `query.{filterable,searchable,sortable}` allowlist is the opt-in for higher-privileged roles; the existing intersection validator gates it at compile time.

**Breaking:** `masking[col].filterable / .searchable / .sortable` is rejected at compile time with a migration hint. Replace with `query.roles` (or delete the flags — `query` defaults to `show.roles`).

---

## v0.9.0 — April 25, 2026

### Unified `read:` pipeline

The read side of every resource has its own block — `read:` — that owns the collection-level `GET /` endpoint, single-record `GET /:id`, and named view projections.

**Breaking changes** (compile-time errors with migration hints):

- `crud.list` → `read.access` (plus `read.pageSize` / `read.maxPageSize`)
- `crud.get` → `read.access` (single-record GET inherits automatically)
- Top-level `views: {...}` → `read.views: {...}`
- `crud.list.fields` / `crud.get.fields` → declare a named view under `read.views` and call `/views/{name}`

### Pre-record access ordering

Handlers on `/:id` run a role-only access check before the firewall query. A caller without a matching role gets 403 before the database is touched. This closes the 404-vs-403 ID-probe channel — an unauthorized caller can no longer distinguish "row exists in another tenant" from "row doesn't exist." See [Access evaluation order](/define/access#evaluation-order).

### Transactional batch operations

Batch endpoints expose `failFast` and a `meta.transactional` response field. On tx-supporting providers (postgres, supabase, neon, libsql, better-sqlite3, bun-sqlite), `failFast: true` wraps the per-record loop in `db.transaction(...)` so any failure rolls back the entire batch. On Cloudflare D1 (no `db.transaction`), `failFast: true` stops on the first error but prior writes stay committed.

Embedding queue messages and realtime broadcasts emit after the transaction commits — rolled-back batches produce no orphan jobs.

See [Batch Operations → Transactional semantics](/api/batch-operations#transactional-semantics).

---

## v0.8.7 — April 17, 2026

### Hono CVE bump

Hono is bumped to `^4.12.14` across the monorepo, the compiler Docker image's pre-installed deps, and every generator path that emits Hono into compiled projects. Older ranges covered the vulnerable window. Recompile to pick up the fix in your generated project's `package.json`.

### Cloudflare Email Sending in public beta

Quickback defaults to the Workers `send_email` binding for Cloudflare deploys. Cloudflare Email Sending has graduated to public beta — production-ready with zero-config. AWS SES remains a fully supported opt-in (`email.provider: 'aws-ses'`) for non-Cloudflare runtimes. See [Email Configuration](/configure#email-configuration).

### Dependency sweep

`wrangler@^4.83.0`, `drizzle-orm@^0.45.2`, `drizzle-kit@^0.31.10`, `zod@^4.3.6`, `@cloudflare/workers-types@^4.20260417.1`.

---

## v0.8.5 — April 15, 2026

### `userRole` access primitive

A new `userRole?: string[]` field in the declarative `Access` config evaluates against `ctx.userRole` (the Better Auth admin-plugin `user.role` column). Distinct from `roles`, which matches `ctx.roles` (org membership).

```typescript
// Org owner OR platform admin
access: { or: [{ roles: ['owner'] }, { userRole: ['admin'] }] }

// Platform admins see SSNs across every org
masking: { ssn: { type: 'ssn', show: { userRole: ['admin'] } } }
```

`userRole` works everywhere `Access` is accepted — CRUD, actions, views, masking, and inside `or` / `and` combinators. See [Access → userRole vs roles](/define/access#userrole-vs-roles).

### CMS admin gate enforced server-side

`cms: { access: "admin" }` previously only hid the CMS link in Account UI and rendered an "Access Denied" screen inside the SPA. Server-side enforcement now applies at every CMS-facing surface: the SPA shell, `/api/v1/schema`, and `custom_view` CRUD.

### Better Auth 1.5 + `@cloudflare/workers-types` compatibility

Generated API code typechecks against the new versions. The org-member role-change broadcast hook is emitted via `organizationHooks.afterUpdateMemberRole` (the supported shape after the 1.5 deprecation of `databaseHooks.member.update.after`). `REALTIME_URL` and `ACCESS_TOKEN` are always emitted as optional bindings when the organization plugin is active.

---

## v0.8.4 — April 14, 2026

### `generateId: "short"` strategy

A compact ID option for tables where short, shareable codes matter more than collision resistance at scale:

```typescript
providers: {
  database: defineDatabase("cloudflare-d1", { generateId: "short" }),
}
```

Generates 6-character alphanumeric IDs (e.g. `a3F9xK`) using nanoid's `customAlphabet` with the full 62-char alphabet (`0-9A-Za-z`). Requires the `nanoid` package.

**When to use:** room codes, invite links, share URLs, ephemeral records.

**When not to use:** high-volume tables. 62^6 ≈ 56.8B combinations means ~238K rows before a 50% collision probability. For large tables stick with `"uuid"`, `"cuid"`, or `"prefixed"`. See [Providers → ID Generation Options](/configure/providers#id-generation-options).

---

## v0.8.2 — April 11, 2026

### Catch-all `VITE_QUICKBACK_URL`

A single Quickback origin can be set in place of four separate URL env vars:

```bash
VITE_QUICKBACK_URL=https://secure.example.com
```

| Individual var (explicit) | Fallback when unset |
|---|---|
| `VITE_QUICKBACK_API_URL` | `${VITE_QUICKBACK_URL}` |
| `VITE_QUICKBACK_ACCOUNT_URL` | `${VITE_QUICKBACK_URL}/account` |
| `VITE_QUICKBACK_CMS_URL` | `${VITE_QUICKBACK_URL}/cms` |

`VITE_QUICKBACK_APP_URL` stays independent — it points at the tenant's own frontend. Non-breaking; individual vars take precedence when set. Library users can set `__QUICKBACK_URL__` on `globalThis`. See [Environment Variables](/ui/account/environment-variables).

---

## v0.8.1 — April 9, 2026

### Role hierarchy with `+` suffix

Define a hierarchy and use `+` to mean "this role and above":

```typescript
auth: { roleHierarchy: ['member', 'admin', 'owner'] }

crud: {
  list:   { access: { roles: ["member+"] } },  // member, admin, owner
  create: { access: { roles: ["admin+"] } },    // admin, owner
  delete: { access: { roles: ["owner"] } },      // owner only
}
```

Roles are expanded at compile time. No `+` = exact match. See [Access → Role Hierarchy](/define/access#role-hierarchy).

---

## v0.8.0 — April 9, 2026

### Breaking: VITE URL environment variables namespaced

All Quickback-generated URL environment variables are namespaced under `VITE_QUICKBACK_*` to avoid collisions with other Vite projects.

| Old | New |
|-----|-----|
| `VITE_API_URL` | `VITE_QUICKBACK_API_URL` |
| `VITE_ACCOUNT_URL` | `VITE_QUICKBACK_ACCOUNT_URL` |
| `VITE_APP_URL` | `VITE_QUICKBACK_APP_URL` |
| `VITE_CMS_URL` | `VITE_QUICKBACK_CMS_URL` |

Non-URL variables are unchanged (`VITE_ENABLE_*`, branding vars, `VITE_STRIPE_PUBLISHABLE_KEY`).

**Action required:**
- Compiled projects: re-run `quickback compile`.
- Standalone deploys: update `.env`, `.env.production`, and `wrangler.toml` files manually.
- Library users: `__QUICKBACK_API_URL__` / `__QUICKBACK_APP_URL__` globals are unchanged.

### CMS access control

`cms.access: "admin"` enforces access at the CMS level, not just link visibility. Non-admin users (`user.role !== "admin"`) see an "Access Denied" screen with a sign-out button.

---

## v0.5.11 — February 24, 2026

### Mandatory unsafe action audit trail

Structured `unsafe` config (`unsafe: { reason, adminOnly, crossTenant, targetScope }`). Cross-tenant unsafe actions require Better Auth authentication plus platform admin role (`ctx.userRole === "admin"`). Mandatory audit logging on success, denial, and error paths. Cloudflare output includes `AUDIT_DB` wiring, `drizzle.audit.config.ts`, and `db:migrate:audit:*` scripts when unsafe actions are present.

Compile-time raw SQL guard for actions and handlers (`allowRawSql: true` required per action).

### Security contract report artifacts

Generated security contract artifacts: `reports/security-contracts.report.json`, `reports/security-contracts.report.sig.json`. Config-driven signing controls (`compiler.securityContracts.report.signature.*`). Missing required signing keys fail loudly with explicit remediation.

### Headless Drizzle rename hints

`compiler.migrations.renames` configuration for CI/CD environments where Drizzle's interactive rename prompts block compilation. See [Configuration](/configure).

### Minor

- Email OTP magic links work correctly.
- Auth is required for all API routes when running locally.
- Single-file multi-table exports alongside `defineTable()` produce a clear error.

---

## v0.5.8 — February 14, 2026

### Quickback CMS

A schema-driven admin panel that connects to your generated API. The compiler now outputs a JSON schema registry consumed by the CMS at runtime. Firewall error modes (`reveal` for 403 with details, `hide` for opaque 404) are configurable per resource for security-sensitive deployments.

---

## v0.5.7 — February 12, 2026

### Scoped database for actions

Actions receive a security-scoped database instead of a raw Drizzle instance. The compiler generates a proxy wrapper that automatically enforces org isolation, owner filtering, and soft-delete visibility — the same protections that CRUD routes have always had.

Duck-typed column detection at runtime:

| Column Detected | SELECT / UPDATE / DELETE | INSERT |
|---|---|---|
| `organizationId` | Adds `WHERE organizationId = ?` | Auto-injects `organizationId` from context |
| `ownerId` | Adds `WHERE ownerId = ?` | Auto-injects `ownerId` from context |
| `deletedAt` | Adds `WHERE deletedAt IS NULL` | — |

Every action is secure by default — no manual `WHERE` clauses needed.

Actions that intentionally need to bypass security (admin reports, cross-org queries, migrations) can declare `unsafe: true` to receive a raw, unscoped database handle via `rawDb`:

> **Retired syntax.** The `defineActions(<table>, {…})` wrapper below was retired by the one-file-per-action change in v0.10.5 — the canonical shape is one `actions/<name>.ts` per action with `export default defineAction({…})`. `unsafe: true` / `rawDb` are unchanged. Kept as the record of what shipped.

```typescript
defineActions(analytics, {
  globalReport: {
    unsafe: true,
    execute: async ({ db, rawDb, ctx, input }) => {
      // db → still scoped (safety net)
      // rawDb → bypasses all security filters
      const allOrgs = await rawDb.select().from(organizations);
    },
  },
});
```

Without `unsafe: true`, `rawDb` is `undefined`. See [Actions](/define/actions).

### Cascading soft delete

Soft-deleting a parent record automatically cascades to child and junction tables within the same feature. The compiler detects foreign key references at build time and generates cascade `UPDATE` statements.

Rules:
- Applies to soft delete only. Hard delete relies on database-level `ON DELETE CASCADE`.
- Cascades within the same feature only — cross-feature references are not affected.
- Child tables must have `deletedAt` / `deletedBy` columns (auto-added by the compiler).

See [Actions API → Cascading Soft Delete](/api/actions#cascading-soft-delete).

### Advanced query parameters

New capabilities for all list endpoints:

- **Field selection** — `?fields=id,name,status`
- **Multi-sort** — `?sort=status:asc,createdAt:desc`
- **Total count** — `?count=true` returns total matching records in `X-Total-Count`
- **Full-text search** — `?search=keyword` searches across all text columns

See [Query Parameters](/api/query-params).

---

## v0.5.5 — February 5, 2026

### OpenAPI spec generation

Generated APIs include a full OpenAPI specification at `/openapi.json`. Better Auth endpoints are included in the spec.

### Better Auth plugins

Published `@quickback-dev/better-auth-upgrade-anonymous` (post-passkey email collection flow), `@quickback-dev/better-auth-combo-auth` (combined email + password + OTP), and `@quickback-dev/better-auth-aws-ses` (AWS SES email provider).

### Security headers

Global error handler prevents leaking internal error details. Security headers middleware (CSP, HSTS, X-Frame-Options, X-Content-Type-Options). `BETTER_AUTH_SECRET` properly passed to generated config.

---

## v0.5.4 — January 30, 2026

### Account UI

Pre-built authentication UI deployed as Cloudflare Workers. Sessions, organizations, passkeys, passwordless, admin panel, API keys. Dual-mode: standalone (degit template) or embedded with Quickback projects.

### Webhook system

Inbound webhook endpoints with signature verification. Outbound webhooks via Cloudflare Queues with automatic retries. Configurable per-feature webhook events.

### Realtime & vector search

Durable Objects + WebSocket realtime subscriptions. Vector embeddings via Cloudflare Vectorize. KV and R2 storage integrations.

---

## v0.5.0 — January 2026

### Initial release

- **Quickback Compiler** — TypeScript-first backend compiler.
- **Four security pillars** — Firewall, Access, Guards, Masking.
- **`defineTable()`** — schema + security configuration in a single file.
- **Templates** — Cloudflare Workers, B2B SaaS.
- **Cloud compiler** — remote compilation via `compiler.quickback.dev`.
- **CLI** — `quickback create`, `quickback compile`.
- **Better Auth integration** — organizations, roles, sessions.
- **Drizzle ORM** — schema-first with automatic migrations.
- **Cloudflare D1** — split database support (auth + features).
