Quickback Docs

v0.18 and earlier

Quickback release notes for v0.5.0 through v0.18.1 (January – May 2026).

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 invitesInviteMemberDialog 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.tsxcan('invitation', 'create' | 'cancel') gates the Invite button, resend, and cancel separately
  • settings.tsxcan('organization', 'update') for the form, can('organization', 'delete') for the danger zone (was owner-only)
  • teams.tsx / teams.$teamId.tsxcan('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.

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.

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:

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

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:

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:

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.

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

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.


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:

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:

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 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:

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.


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::

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 and Access → Relationship-based authorization.

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:

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:

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.


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:

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.


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. 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.

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 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:

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.


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:

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:

TriggerAuto-addedDirective
cms: true OR account: true'unsafe-inline', https://fonts.googleapis.comstyleSrc
cms: true OR account: truehttps://fonts.gstatic.comfontSrc
csp set (always)@trustedOriginsconnectSrc
Bundled SPA enabledSPA-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:

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):

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.

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:

// 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.tsrecord, 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.

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:

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:

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().

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:

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.listread.access (plus read.pageSize / read.maxPageSize)
  • crud.getread.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.

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.


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.

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).

// 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.

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:

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.


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:

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.


v0.8.1 — April 9, 2026

Role hierarchy with + suffix

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

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.


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.

OldNew
VITE_API_URLVITE_QUICKBACK_API_URL
VITE_ACCOUNT_URLVITE_QUICKBACK_ACCOUNT_URL
VITE_APP_URLVITE_QUICKBACK_APP_URL
VITE_CMS_URLVITE_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.

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 DetectedSELECT / UPDATE / DELETEINSERT
organizationIdAdds WHERE organizationId = ?Auto-injects organizationId from context
ownerIdAdds WHERE ownerId = ?Auto-injects ownerId from context
deletedAtAdds 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.

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.

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.

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.


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.

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.
  • CLIquickback create, quickback compile.
  • Better Auth integration — organizations, roles, sessions.
  • Drizzle ORM — schema-first with automatic migrations.
  • Cloudflare D1 — split database support (auth + features).

On this page

v0.18.1 — May 18, 2026Account SPA — teams, active-team picker, permission-aware UITeams (opt-in)Permission-aware UI via checkRolePermissionEditable slug + metadata on organization settingsAuth client wiringv0.18.0 — May 17, 2026config.apps — mount user-authored SPAs on dedicated hostnamesCross-subdomain cookie inference handles apex aliasesv0.17.2 — May 15, 2026customPlugins escape hatch on defineAuth("better-auth", …)v0.17.1 — May 14, 2026quickback start — interactive onboardingNamed views on the free tierquickback init removedMinorv0.16.10 — May 14, 2026D1-specific safety pass on generated migrationsv0.16.9 — May 13, 2026Resource-scoped realtime ticketsUnified broadcast targetingv0.16.8 — May 13, 2026One auth transport per requestCookie-backed CSRF gateFirst-class Wrangler observabilityv0.16.7 — May 12, 2026Bearer wins over cookieBearer plugin defaults to requireSignature: truev0.16.5 — May 12, 2026Flat write DSLCMS placement for multi-table feature actionsopenapi.types accepts a string arrayStable artifact orderingv0.16.4 — May 12, 2026Drizzle snapshot integrityaccount.auth.* drives the server, not just the SPAv0.16.3 — May 9, 2026Standalone action mount orderv0.16.1 — May 9, 2026Cloudflare ASSETS canonicalizationaccount.customLoginProject-level realtime defaultsCompile-time validatorsDrizzle metadata hardeningMinorv0.16.0 — May 9, 2026Agent Auth ProtocolBearer-only on /mcp and the agent-auth surfaceAuth DSL gains first-class plugin entriesqueue-consumer.ts gatingv0.15.0 — May 7, 2026Sysadmin tier for the CMSDurable Object [[migrations]] order is preservedquickback/public/ — static-asset passthroughMinorv0.14.7 — May 7, 2026Codegen fixes for projects with non-id primary keys and DO-only featuresemailOtp + AWS SES is OTP-onlyv0.14.6 — May 7, 2026MCP transport correctnessv0.14.5 — May 7, 2026MCP environment threadingv0.14.4 — May 7, 2026via: firewall predicates pull required importsv0.14.3 — May 7, 2026Relationship-role table re-exportsv0.14.2 — May 7, 2026Historical DO [[migrations]] are preservedv0.14.1 — May 7, 2026Bulk action variants in openapi.jsonCross-tenant "my events" patternsv0.14.0 — May 7, 2026Relationship-based authorizationBulk variants on record-based actionsAggregations on read viewsv0.13.5 — May 6, 2026Object-form auth.plugins config is honoredroles: ["PUBLIC"] allowed on writes and actionsv0.12.0 — May 5, 2026Per-room authorization for PartyServerv0.11.1 — May 5, 2026PartyServer auto-mount is secure by defaultv0.11.0 — May 5, 2026OAuth Provider (MCP-ready)API keys via query parameterPartyServer rooms + realtime URL splitv0.10.20 — May 4, 2026In-worker Durable Objectsv0.10.18 — May 2, 2026experimentalRemote — share deployed D1 with wrangler devGoogle OAuth + signup ToSCLI compile auth gatev0.10.15–v0.10.17 — May 2, 2026CSP auto-configv0.10.14 — May 1, 2026auth.jwt configAdmin MCP endpoints filteredDeclarative security configHSTS skipped on localhostv0.10.13 — May 1, 2026Clickjacking defence on every Workerv0.10.11 — May 1, 2026MCP tools/list is access-filteredv0.10.10 — April 29, 2026Reserved keys in access.record are rejectedv0.10.8 — April 29, 2026Audit logging on Postgres targetsauditDatabaseId is a compile-time requirementAction input → OpenAPI / MCPOpenAPI fidelityMinorv0.10.5 — April 28, 2026One file per actionMulti-table actions per featureFirewall: named-scope shapev0.10.0 — April 26, 2026q.scope() — single-declaration tenant scopingmasking[col].query.roles — unified filter / sort / search gatev0.9.0 — April 25, 2026Unified read: pipelinePre-record access orderingTransactional batch operationsv0.8.7 — April 17, 2026Hono CVE bumpCloudflare Email Sending in public betaDependency sweepv0.8.5 — April 15, 2026userRole access primitiveCMS admin gate enforced server-sideBetter Auth 1.5 + @cloudflare/workers-types compatibilityv0.8.4 — April 14, 2026generateId: "short" strategyv0.8.2 — April 11, 2026Catch-all VITE_QUICKBACK_URLv0.8.1 — April 9, 2026Role hierarchy with + suffixv0.8.0 — April 9, 2026Breaking: VITE URL environment variables namespacedCMS access controlv0.5.11 — February 24, 2026Mandatory unsafe action audit trailSecurity contract report artifactsHeadless Drizzle rename hintsMinorv0.5.8 — February 14, 2026Quickback CMSv0.5.7 — February 12, 2026Scoped database for actionsCascading soft deleteAdvanced query parametersv0.5.5 — February 5, 2026OpenAPI spec generationBetter Auth pluginsSecurity headersv0.5.4 — January 30, 2026Account UIWebhook systemRealtime & vector searchv0.5.0 — January 2026Initial release