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" (fromlistUserTeams) and "Other teams" so non-admins land on what's relevant. Create / rename / delete from the list, gated onteam:create/team:update/team:delete. - Team detail (
/$slug/teams/$teamId) — member roster with add / remove, joininglistTeamMembersagainst the org's full member list for display. - Active-team picker in the org header — calls
setActiveTeamand exposes the choice viaOrganizationContext.activeTeamIdso downstream routes (and the BAsession.activeTeamIdserver-side) stay in sync. - Team-aware invites —
InviteMemberDialogshows an optional team picker when teams are on and forwardsteamIdtoinviteMember, 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.tsxtab gates →can('invitation', 'create'),can('organization', 'update')members.tsxaction 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 enforcementinvitations.tsx→can('invitation', 'create' | 'cancel')gates the Invite button, resend, and cancel separatelysettings.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
checkSlugflow (mirroring the create-org form); on rename, the SPA navigates to/${newSlug}/settingsand the layout re-fetches with the new slug. - Metadata — single
Record<string, any> | nullJSON 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 = trueroute inwrangler.tomlfor the primary hostname and every alias - adds each hostname to Better Auth's
TRUSTED_ORIGINS(whentrustedOriginsis left unset) - widens the CSP
connect-srcto 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:
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 startstart 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 whoamireads the stored tier from your credentials.- Pro upsell copy in
quickback loginis 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.
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.
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 featurecms.placement: "tables"withcms.tablesscopes it to specific tablescms.hidden: truekeeps 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.enabledemailVerification → emailAndPassword.requireEmailVerificationpasskey/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, useUSER, or split the route).createRealtimeimports withoutrealtime: trueenabled 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
SYSADMINis now recognised by the standalone-action org gate's pseudo-role matcher.- Apex
/and/account/loginget fallback redirects toauth.loginUrlwhen 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
queueexport 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.
dbreferences in single-record and bulk action handlers now resolve correctly during pre-fetch.- PartyServer auth gates pass the in-scope
dbhandle 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/callURLs are no longer double-prefixed for action tools.- Auth middleware accepts API keys via
Authorization: Bearerin addition tox-api-keyand query parameters — the channel MCP clients use exclusively. auth.roleHierarchyis 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:
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_URLenv var renamed toBROADCAST_URL.- SPA helpers
getRealtimeWsUrl()/getRealtimeTicketUrl()renamed togetBroadcastWsUrl()/getBroadcastTicketUrl(). appConfig.routes.api.realtimerenamed toappConfig.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:
| 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:
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 /mcpno longer enumerates tool names. - Internal flavor text is removed;
annotations.destructiveHintand_unsaferemain — 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: [...] }.idclaimsformat: "uuid"only when the project'sgenerateIdactually 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
QuickbackErrorCodeenum values (DB_NOT_NULL_VIOLATION,DB_UNIQUE_VIOLATION, etc.) so generated SDKs can exhaustively switch oncode. 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.ts — record, input, and audit fields are typed without manual annotations.
Discovery rules:
actions/<name>.tsbinds to the feature's primary table.actions/<table>/<name>.tsbinds 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 bundledactions.ts, andactions/<table>.ts— plus a universal_feature.tsslot. 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:
ownerIdis the auto-detected owner column (the compiler firewalls reads withWHERE ownerId = ctx.userIdand auto-stamps the column on insert) anduserIdis 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. IfuserIdis the only candidate column the compiler raises an error; alongside a detected isolation column it raises a warning. Two escapes: rename the column toownerId, or declarefirewall: { 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.list→read.access(plusread.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 underread.viewsand 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.
| 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, andwrangler.tomlfiles 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 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 oneactions/<name>.tsper action withexport default defineAction({…}).unsafe: true/rawDbare 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/deletedBycolumns (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=truereturns total matching records inX-Total-Count - Full-text search —
?search=keywordsearches 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.
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).