Changelog
Release notes and version history for the Quickback compiler, CLI, and platform.
Changelog
Release notes for the Quickback compiler, CLI, and platform.
v0.8.7 — April 17, 2026
Security: Hono CVE cluster fix
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 (^4.0.0 / ^4.5.0 / ^4.6.0 / ^4.7.11) covered the vulnerable window. Recompile to pick up the fix in your generated project's package.json.
Cloudflare Email Sending is now in public beta
Quickback already defaults to the Workers send_email binding for Cloudflare deploys, and Cloudflare Email Sending has now graduated to public beta — production-ready with zero-config, no API keys, no third-party accounts. No action needed; recompiling any existing project continues to wire the default Cloudflare transport. AWS SES remains a fully supported opt-in (email.provider: 'aws-ses') for non-Cloudflare runtimes or existing SES setups. See Email Configuration.
Minor/patch dep sweep
Aligned the whole dep surface — monorepo, compiler Docker image, and generator emissions into compiled projects — so new compiles and existing recompiles converge on the same versions:
wrangler@^4.83.0drizzle-orm@^0.45.2,drizzle-kit@^0.31.10zod@^4.3.6@cloudflare/workers-types@^4.20260417.1(also deduped — was accidentally declared in bothdependenciesanddevDependenciesin some paths)@tanstack/react-query@^5.99.0(Account app)
v0.8.5 — April 15, 2026
Fix: Better Auth 1.5 and @cloudflare/workers-types Compatibility
Generated API code failed to typecheck after Better Auth bumped to 1.5 and @cloudflare/workers-types tightened BufferSource to ArrayBufferView<ArrayBuffer>. Four unrelated surfaces broke:
databaseHooks.member.update.afterwas removed in Better Auth 1.5. The compiler now emits the member role-change broadcast hook asorganizationHooks.afterUpdateMemberRoleinside theorganization()plugin call, which is the current supported shape.REALTIME_URLandACCESS_TOKENare always emitted as optionalCloudflareBindingsfields when the organization plugin is active, so the broadcast hook typechecks even whenrealtimeModeis"inline"(or realtime isn't configured at all).auth.api.listOrganizations/getFullOrganization/session.session.activeOrganizationId— third-party Better Auth plugins narrow the inferredauth.apitype and hide organization-plugin endpoints. The generated middleware and/admin/v1/organizationsroute now cast throughanyfor these calls.crypto.subtle.sign/verifyno longer acceptUint8Array<ArrayBufferLike>directly. Generatedjwt.tscasts encoded payloads toBufferSource.
No config changes required — recompile to pick up the fixes.
CMS Admin Gate Now Enforced Server-Side
cms: { access: "admin" } previously only hid the CMS link in Account UI and rendered an "Access Denied" screen inside the SPA — the static assets, /api/v1/schema, and /api/v1/custom_view CRUD were reachable by any authenticated user. A non-admin with an API token or cookie could bypass the UI and read schema metadata or saved CMS views directly.
This release adds server-side enforcement at every CMS-facing surface:
- CMS SPA shell — the Worker checks
ctx.userRole === "admin"before servingindex.html. Unauthenticated requests redirect to login; authenticated non-admins get403 Admin access required. Static bundles (JS/CSS) remain reachable so browser caches aren't invalidated. /api/v1/schema— returns403to non-admins.custom_viewCRUD — now gated on the newuserRole: ["admin"]primitive rather than org membership.- In-SPA gate — unchanged; defense in depth behind the server-side gates.
Your resource CRUD endpoints are unaffected — they keep their per-resource crud.access.roles.
New userRole Access Primitive
Added userRole?: string[] to the declarative Access config, evaluated against ctx.userRole (the Better Auth admin-plugin user.role column). This is distinct from roles, which matches ctx.roles (org membership).
// Old: in multi-tenant this meant org-admin, not platform-admin
access: { roles: ['admin'] }
// New: explicitly require user.role === 'admin' regardless of org membership
access: { userRole: ['admin'] }
// "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.
Fix: ReferenceError: customAlphabet is not defined in generated POST handlers
Projects with providers.database.config.generateId: "short" (or "nanoid" / "cuid") compiled routes that called the ID helper but never imported it. Every generated src/features/**/*.routes.ts had customAlphabet('0123...', 6)() in its POST body but no matching import { customAlphabet } from 'nanoid'; at the top of the file. Every POST /api/v1/<resource> and batch-create returned 500 with ReferenceError: customAlphabet is not defined on the first write.
The compiler now emits the correct import alongside the ID expression for all three modes: nanoid, customAlphabet, and createId from @paralleldrive/cuid2.
No config changes required — recompile to pick up the fix.
Fix: Dangling export const in generated actions.ts breaks tsc
When a feature authored actions as export const xActions = defineActions(...) (instead of export default defineActions(...)), the compiler's preamble extractor leaked the assignment's left-hand side into the generated src/features/<feature>/actions.ts. The output contained a dangling export const xActions = with no right-hand side, and tsc --noEmit failed with TS1109: Expression expected. Runtime happened to work because the unused placeholder was never evaluated, but typecheck was red.
The preamble extractor now anchors its boundary to the start of the top-level statement containing defineActions(...), covering export default, export const X =, const X =, and bare call forms. Local Zod helper declarations above the call continue to be preserved.
Fix: Files worker database_id reverted to "local-files" on every compile
Projects using the two-worker R2 architecture had their files worker's wrangler.toml (apps/backend/cloudflare-workers/files/wrangler.toml) rewritten with database_id = "local-files" on every quickback compile, even when providers.database.config.filesDatabaseId was set to a real D1 UUID. The main worker's binding was correct — only the files worker fell through. Remote deploys of the files worker bound to a non-existent D1, so file serving and access-control reads failed with a binding error.
The files-worker wrangler generator now reads filesDatabaseId (and filesDatabaseName) from providers.database.config as a fallback when they aren't set on providers.fileStorage.config, matching the precedence chain already used by the main worker. Local dev is unaffected — wrangler dev --local uses .wrangler/state/ regardless of the configured ID.
No config changes required — recompile and redeploy the files worker.
v0.8.6 — April 16, 2026
Fix: Audit columns silently dropped from tables with a same-line index callback
Tables authored with the composite-index shape Drizzle / prettier produces —
export const episodeTags = sqliteTable("episode_tags", {
id: text("id").primaryKey(),
episodeId: text("episode_id").notNull(),
tagId: text("tag_id").notNull(),
}, (t) => [ // <-- "}, (t) =>" on one line
uniqueIndex("episode_tag_unique").on(t.episodeId, t.tagId),
]);never received audit field injection. A regex in injectAuditFields required a literal \n between the columns-object }, and the (t) => index callback, so same-line closers were silently skipped and the function returned the source unchanged. drizzle-kit then diffed the audit-less schema against the previous snapshot and emitted ALTER TABLE ... DROP COLUMN created_at/modified_at/deleted_at/created_by/modified_by/deleted_by for every affected table. Any project that applied those migrations had production CRUD return 500 because the generated firewall helper still referenced deletedAt.
The regex is now whitespace-flexible and the identifier class accepts any valid JS identifier (t, table, _t, tbl2). A defensive guard throws at compile time if all three terminator patterns fail on a recognizable Drizzle table — no more silent drops.
Recovery path:
- Most projects: recompile. drizzle-kit will detect the audit columns are missing from the DB snapshot and emit an
ADD COLUMNmigration to restore them. Apply it. - Projects that already hand-patched their schema and ran a manual
ADD COLUMNmigration (like thepodcast-appworkaround in commit98d8758): the next compile will regenerate a matchingADD COLUMNmigration that will fail with "duplicate column name" against a DB that already has the columns. Mark the new migration as applied without running it (e.g.INSERT INTO __drizzle_migrationsin the features DB) or hand-edit the generated SQL to a no-op, then continue normally.
Fix: firewall.exception generated isNull(<table>.deletedAt) against a missing column
Resources with firewall: { exception: true } always emitted return isNull(<table>.deletedAt); inside buildFirewallConditions(), regardless of whether the table actually had a deletedAt column. Projects that disabled audit injection globally (compiler.features.auditFields: false) or authored a schema without deletedAt ended up with Drizzle resolving <table>.deletedAt to undefined at query time — every GET /api/v1/<resource> returned 500.
The three concerns are now fully independent:
firewall.exception: true— only disables tenant scoping (org/user/teamWHEREclauses). It does not gate audit fields or soft-delete.- Audit fields — always injected unless globally opted out. Unchanged.
- Soft-delete filter — emitted only when the schema actually has
deletedAtor the user explicitly setfirewall.softDelete.
In the default configuration (audit injection on), behavior is unchanged: exception resources still filter deletedAt IS NULL. Only the broken case changes — exception resources on a schema without deletedAt now return undefined (no firewall), matching the column reality.
No config changes required — recompile to pick up the fix.
v0.8.4 — April 14, 2026
New generateId: "short" Strategy
Added 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 in your project (same as the existing "nanoid" strategy).
When to use: room codes, invite links, share URLs, ephemeral records — anywhere the ID appears in a URL or needs to be human-typeable.
When not to use: high-volume tables. 62^6 ≈ 56.8B combinations means ~238K rows before a 50% collision probability (birthday bound). For large tables stick with "uuid", "cuid", or "prefixed".
See Providers - ID Generation Options for the full list of strategies.
v0.8.3 — April 14, 2026
Fix: wrangler.toml Route Placement and Binding IDs
Two bugs in the generated wrangler.toml that blocked production deployment without manual edits:
Routes were nested under [dev]. The routes = [...] array was emitted after the [dev] section header, so TOML parsed it as dev.routes and wrangler ignored the custom domain (warning: Unexpected fields found in dev field: "routes"). Routes now emit as a top-level key.
D1 and KV bindings used placeholder IDs. Despite databaseId, filesDatabaseId, and kvId being set in the quickback config, the generated wrangler.toml wrote "local-features", "local-files", and "local" instead of the real UUIDs. The compiler now resolves IDs from multiple config sources:
| Binding | Config sources checked (in order) |
|---|---|
AUTH_DB | database.config.authDatabaseId |
DB | database.config.featuresDatabaseId, database.config.databaseId |
FILES_DB | fileStorage.config.filesDatabaseId, database.config.filesDatabaseId |
KV | database.config.kvNamespaceId, storage.config.namespaceId, auth.config.kvId |
See Providers - Database Options for the full list of supported ID properties.
Fix: Account SPA Post-Login Redirect on Unified Domain
When the Account SPA was served at /account/ on the unified quickback domain (e.g. secure.example.com/account/), post-login redirects dropped the basepath and sent users to /dashboard instead of /account/dashboard. Sign-out redirects had the same issue.
The SPA now shares a basepath module across the router and all raw window.location.href assignments, so login, signup, email OTP, welcome, and sign-out flows all stay under the correct path prefix regardless of whether the SPA is on the unified domain or its own dedicated auth subdomain.
No config changes required — recompile to pick up the fix.
v0.8.2 — April 11, 2026
Catch-All VITE_QUICKBACK_URL Environment Variable
Instead of setting four separate URL env vars, you can now point at a single Quickback origin:
# One variable — done.
VITE_QUICKBACK_URL=https://secure.example.comWhen set, it's used as a fallback for the individual URLs:
| 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, not the Quickback server.
Non-breaking. Individual vars still take precedence when set, so existing configurations work unchanged. The compiler now also emits VITE_QUICKBACK_URL into the generated SPA .env files, and library users can set __QUICKBACK_URL__ on globalThis for the same effect.
See Environment Variables for the full reference.
v0.8.1 — April 9, 2026
Role Hierarchy with + Suffix
You can now define a role hierarchy in your config and use the + suffix in access rules to mean "this role and above":
// quickback.config.ts
auth: {
roleHierarchy: ['member', 'admin', 'owner'],
}
// In resource definitions
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 for details.
v0.8.0 — April 9, 2026
Breaking: VITE URL Environment Variables Renamed
All Quickback-generated URL environment variables are now 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_* feature flags, branding vars (VITE_APP_NAME, etc.), and VITE_STRIPE_PUBLISHABLE_KEY stay as-is.
Action required:
- Compiled projects — re-run
quickback compile. The compiler generates the new names automatically. - Standalone deploys — update
.env,.env.production, andwrangler.tomlfiles manually. - Library users — the
__QUICKBACK_API_URL__/__QUICKBACK_APP_URL__globalThis names are unchanged, but if you pipe Vite env vars into them, update the var names.
See Environment Variables for the full reference.
CMS Access Control
The cms.access config option now enforces access at the CMS level, not just link visibility in Account UI.
cms: { access: "admin" }When set to "admin", non-admin users (user.role !== "admin") see an "Access Denied" screen with a sign-out button instead of the CMS. When set to "user" (the default), any authenticated user can access the CMS.
See CMS Connecting and Compiler Config for details.
SPA Sub-Route Fix
Fixed a bug where hard-refreshing or directly navigating to SPA sub-routes (e.g., /account/profile, /cms/tables/users) returned a blank white screen. The Worker now correctly serves index.html for all SPA paths, allowing the client-side router to handle routing.
v0.5.11 — February 24, 2026
Email OTP & Auth Fixes
- Fixed email-OTP magic links not working correctly
- Removed deprecated
/internal/validateendpoint — use standard Better Auth session validation instead - Auth is now required for all API routes when running locally (previously some routes were unprotected in dev)
Multiple Table Exports Fix
- Fixed a compiler error when a single file exports multiple Drizzle tables alongside a
defineTable()default export - The CLI now properly detects and reports this with a clear error message pointing to the fix
Headless Drizzle Rename Hints
- Added
compiler.migrations.renamesconfiguration for CI/CD environments where Drizzle's interactive rename prompts would block compilation - Compile errors now include explicit rename key paths and fail fast on malformed rename config keys
- See Configuration for details
Security Contract Report Artifacts
- Added generated security contract artifacts to compiler output:
reports/security-contracts.report.jsonreports/security-contracts.report.sig.json
- Added config-driven signing controls:
compiler.securityContracts.report.signature.enabledcompiler.securityContracts.report.signature.requiredcompiler.securityContracts.report.signature.key/keyEnv/keyId
- Missing required signing keys now fail loudly with explicit remediation guidance
- Added strict config validation for report/signature paths and signing options
Mandatory Unsafe Action Audit Trail
- Added structured unsafe action config (
unsafe: { reason, adminOnly, crossTenant, targetScope }) - Cross-tenant unsafe actions now require Better Auth authentication plus platform admin role (
ctx.userRole === "admin") - Added mandatory audit logging for unsafe cross-tenant actions (success, denial, and error paths)
- Cloudflare output now includes optional
AUDIT_DBwiring,drizzle.audit.config.ts, anddb:migrate:audit:*scripts when unsafe actions are present - Added compile-time raw SQL guard for actions and handlers (
allowRawSql: truerequired per action)
v0.5.10 — February 19, 2026
Compiler Page Parsing & CLI Output
- Improved compiler page parsing for
definePage()definitions - Better CLI output formatting during compilation
- Added
apiPathto schema registry for CMS integration
Bug Fixes
- Fixed hyphenated action names not being properly quoted in generated code (e.g.,
mark-completenow generates valid JavaScript) - API key authentication (
x-api-keyheader) is now handled separately from session tokens (Bearerheader)
v0.5.9 — February 16, 2026
CMS Pages & CLI Page Support
- Added
definePage()support for CMS-managed pages - Auth middleware improvements for page routes
- CLI now supports page definitions alongside table and action definitions
v0.5.8 — February 14, 2026
CMS App
- Introduced the Quickback CMS — a schema-driven admin panel that connects to your generated API
- CMS namespace added to actions for admin-specific operations
- Fixed
guard→accessnaming inconsistency in CMS action definitions
Schema Registry & Firewall Improvements
- Added schema registry generator — the compiler now outputs a JSON schema registry used by the CMS
- Firewall error modes: choose between
reveal(403 with details) andhide(opaque 404) for security-sensitive deployments
Bug Fixes
- Fixed anonymous user email format generation
- Organization selector improvements in Account UI
- Config validation now catches more errors at compile time
- Better CRUD error handling with structured error responses
- Fixed masking to use representative star counts instead of fixed formatting
v0.5.7 — February 12, 2026
Scoped Database for Actions
Actions now 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.
The scoped DB uses 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 | — |
This means every action is secure by default — no manual WHERE clauses needed.
defineActions(todos, {
complete: {
type: "record",
execute: async ({ db, ctx, record, input }) => {
// db is scoped — only sees records in user's org, excludes soft-deleted
const siblings = await db.select().from(todos);
// ↑ automatically filtered to ctx.activeOrgId + deletedAt IS NULL
},
},
});Unsafe Mode
Actions that intentionally need to bypass security (admin reports, cross-org queries, migrations) can declare unsafe: true to receive a raw, unscoped database handle:
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.
Related docs: Actions, Actions API
Cascading Soft Delete
Soft-deleting a parent record now 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.
DELETE /api/v1/projects/:idGenerated behavior:
// 1. Soft delete the parent
await db.update(projects)
.set({ deletedAt: now, deletedBy: userId })
.where(eq(projects.id, id));
// 2. Auto-cascade to children (compiler-generated)
await db.update(projectMembers)
.set({ deletedAt: now, deletedBy: userId })
.where(eq(projectMembers.projectId, id));
await db.update(projectTasks)
.set({ deletedAt: now, deletedBy: userId })
.where(eq(projectTasks.projectId, id));Rules:
- Only applies to soft delete (the default). Hard delete relies on database-level
ON DELETE CASCADE. - Only cascades within the same feature — cross-feature references are not affected.
- Child tables must have
deletedAt/deletedBycolumns (auto-added by the compiler's audit fields).
Related docs: Actions API — Cascading Soft Delete
Advanced Query Parameters
New query parameter capabilities for all list endpoints:
- Field selection —
?fields=id,name,statusreturns only the columns you need - Multi-sort —
?sort=status:asc,createdAt:descsorts by multiple fields - Total count —
?count=truereturns total matching records in response headers (X-Total-Count) - Full-text search —
?search=keywordsearches across all text columns
# Get only names and statuses, sorted by status then date, with total count
GET /api/v1/todos?fields=id,name,status&sort=status:asc,createdAt:desc&count=true
# Search across all text fields
GET /api/v1/todos?search=urgentRelated docs: Query Parameters
Audit Field Improvements
deletedAtanddeletedByfields are now always injected by the compiler for tables with soft delete enabled — no need to define them in your schema- All audit fields (
createdAt,createdBy,modifiedAt,modifiedBy,deletedAt,deletedBy) are auto-managed
v0.5.6 — February 8, 2026
Database Naming Conventions
- Default table and column naming changed to snake_case with
usePlurals: false - Table names derived from generated Better Auth schema for consistency
- Removed legacy single-database mode — split databases (auth + features) is now the standard
Auth Variable Shadowing Fix
- Fixed
membervariable in auth middleware that shadowed the Drizzlemembertable import - Renamed to
sessionMemberto avoid conflicts in generated routes
v0.5.5 — February 5, 2026
Better Auth Plugins
- Published
@kardoe/better-auth-upgrade-anonymousv1.1.0 — post-passkey email collection flow - Published
@kardoe/better-auth-combo-auth— combined email + password + OTP authentication - Published
@kardoe/better-auth-aws-ses— AWS SES email provider for Better Auth
OpenAPI Spec Generation
- Generated APIs now include a full OpenAPI specification at
/openapi.json - Better Auth endpoints included in the spec
- Runtime route:
GET /openapi.json
Security Hardening
- Global error handler prevents leaking internal error details
- Security headers middleware (CSP, HSTS, X-Frame-Options, X-Content-Type-Options)
BETTER_AUTH_SECRETproperly passed to generated config
v0.5.4 — January 30, 2026
Account UI
- Pre-built authentication UI deployed as Cloudflare Workers
- Features: 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, Bun standalone, B2B SaaS
- Cloud Compiler — Remote compilation via
compiler.quickback.dev - CLI —
quickback create,quickback compile,quickback init - Better Auth Integration — Organizations, roles, sessions
- Drizzle ORM — Schema-first with automatic migrations
- Cloudflare D1 — Split database support (auth + features)