Quickback Docs

v0.50 – v0.54

Quickback release notes for v0.50.0 through v0.54.0 (July 2026).

v0.54.0 — July 17, 2026

Realtime: named invalidations

Actions can now fire typed, targeted "this changed — refresh" signals after commit. A defineRealtime registry declares typed events (wire name, contract version, named variants with strict Zod payloads); a defineRealtimeDelivery profile sets the maximum audience (a scope room or the org/user lane + required roles); and an optional defineRealtimeRecipients resolver narrows delivery to a resolved identity set — post-commit on the firewalled db, intersecting scope ∩ roles ∩ recipients, fail-closed (a throw or [] reaches nobody, never a wider audience). Bind them with invalidates: on an action and call invalidate.<binding>.<variant>(payload) inside execute. Delivery is after-commit + best-effort (discarded on throw/dryRun), projects the payload to declared keys, and works on every action kind — standalone, per-record, bulk, and namespace-hoisted. See Named invalidations.

Access: SESSION and SCOPED pseudo-role tiers

AUTHENTICATED is now explicitly the wrapper over two auth flavors: SESSION (a full Better Auth session only) and SCOPED (a sessionless scope-token principal only — e.g. an event attendee with no account). Use the specific tier when a route must admit or exclude one flavor; AUTHENTICATED still admits both and excludes only external delegated principals. See Access → Pseudo-roles.

Access: fail-closed scope/firewall coherence guardrail

Granting a scope caller (SCOPED or a scope:<kind>:<role> role) on a resource whose firewall can't filter that caller now fails the build instead of silently returning 200 + [] — the trap that pushes apps toward a fake "active organization." The fix is named in the error: compose org or scope in an { any: [...] } firewall group, use a via: relationship, or firewall: [{ exception: true }].

v0.53.0 — July 17, 2026

Realtime: the broadcast surface tracks contract.version

The /api and /auth bases already move to /api/v2 and /auth/v2 under contract.version: 'v2', but the realtime/broadcast surface stayed pinned at /broadcast/v1 — so a v2 project's WebSocket mount, ws-ticket mint, DO route matcher, notify helper, auth skip-set, and discovery all disagreed with the rest of the contract. The broadcast base now derives from the same resolved contract version (single source of truth in resolveSystemRoutes), threaded through every emitter:

  • contract v1 (or omitted)/broadcast/v1 everywhere, byte-identical to prior output.
  • contract v2/broadcast/v2 everywhere, with no hidden v1 alias.

An explicit compiler.routes.broadcast override still wins. Existing v1 projects are unaffected.

Web Push: VAPID delivery, queue fan-out, and identity-tag targeting

push.provider: 'web-push' turns on a full push pillar: a self-contained delivery module (RFC 8292 VAPID JWTs + RFC 8291 aes128gcm, crypto.subtle only — no Node polyfills), a Cloudflare Queues fan-out with a dead-letter queue, and a hardened push_subscriptions schema. The crypto core is verified against the RFC 8291 Appendix A known-answer vector — the emitted module reproduces the RFC's ciphertext byte-for-byte.

  • Send pathenqueuePushFanout(queue, target, payload) from an action's after-commit phase (effects.enqueue), so rolled-back writes never notify and dryRun sends nothing. Declarative targets ({ userIds }, { targetTags }, { targetRoles }, { all: true }) resolve server-side in the queue consumer — subscription crypto never enters action code. Targeting speaks the realtime identity-tag dialect and is fail-closed: [] selects nobody, broadcast requires an explicit { all: true }.
  • Default schema (push.defaultSchema: true) — owner-firewalled, INTERNAL-read push_subscriptions with a views.mine device list. Creates validate https: endpoints (SSRF blocker), 65-byte P-256 p256dh, 16-byte auth; tags is stamped server-side from the session's proven identity (client tags ignored); 10/min create rate limit; per-user cap keeps the newest 10 live subscriptions.
  • Fail-closed config — a literal vapidPrivateKey in config is a compile error (env:VAPID_PRIVATE_KEY only); the worker refuses to boot without the three VAPID vars; the private key never lands in wrangler.toml, SPA bundles, or logs (DLQ chunks log counts only).
  • SPA bake — the VAPID public key ships to CMS/Account bundles as VITE_QUICKBACK_VAPID_PUBLIC_KEY and window.__QUICKBACK_RUNTIME.vapidPublicKey.
  • CLIquickback push generate-keys [--out <file>]; --out writes the private key to a 0600 file so it never touches stdout or shell history.

See Web Push for the full guide.

v0.52.1 — July 17, 2026

CLI: cold-start tolerance for the cloud compiler

Right after a service deploy, the first compile pays a fresh container image pull — which routinely exceeds the CLI's old 60-second readiness budget and surfaced as Compiler container did not become ready in time even while /health was green. waitForReady now allows 4 minutes, reports elapsed warm-up progress, and the timeout message explains the deploy-in-progress case.

v0.52.0 — July 17, 2026

Five action primitives: afterCommit, realtime emit, transitions v2, refs, bundles

One release, five primitives that collapse the most-repeated hand-written patterns in real Quickback projects (measured across a 349-action production corpus). Every one is additive — existing projects compile unchanged, and plain { field, fromTo, to|via } transitions keep their emission byte-identical.

  • afterCommit hooks + effects.enqueue — named best-effort side effects that run after execute returns. Each hook is independently wrapped (failures log as [<action>] afterCommit:<hook> failed, never touching the response), non-blocking hooks ride executionCtx.waitUntil, { blocking: true } awaits before responding, and everything skips when execute throws or returns dryRun: true. The execute context also gains effects.enqueue(name, fn) for mid-execute captures and now — one ISO timestamp per invocation. Replaces the per-leg try/catch + waitUntil idiom. See After-commit hooks.

  • Realtime emit for custom actions — tables that lock auto-CRUD no longer hand-roll frames. realtime: { emitFromActions: true, scopeKeyFrom: "events:{eventId}" } makes scoped-db writes inside record-bound and bulk actions emit auto-CRUD-byte-compatible frames after commit (masking + audience applied at fanout; the fail-closed audience requirement extends to action emission). Actions can also declare emit: for a pre-bound emitCrud(row, oldRow?). Updates/deletes pre-select old rows on the same WHERE; raw unsafeDb writes stay invisible. See When auto-broadcast fires.

  • Transitions v2transition grows guard (equality / null / notNull / custom(record)), onIllegal { code, status, message }, idempotent: "noop", and stamp/clears that ride a compiler-applied, TOCTOU-safe UPDATE (guards fold into the WHERE; a lost race is 409 ACCESS_TRANSITION_LOST). With an applied write, execute becomes optional. Table-level transitions: { publish: { field, from, to, access } } generates the action files — including undo: inverses and onEnter cascades — mounted exactly where hand-written files would be (explicit files win, with a warning). The ten byte-identical publish/revert files in the podcast fixture become two lines per entity. See Transitions.

  • refs — scoped foreign-key inputs — the "load the id, 404 if missing, assert it belongs to this event" preamble becomes declarative. Loads run through the scoped db (a cross-org id 404s by construction), batch into one Promise.all, assert matchScope / cross-input matchWith parenthood, and inject rows under their as names. Ref keys are validated against the inline z.object input (with agreeing optionality) at compile time. See Refs.

  • defineBundle — single-round-trip bootstrap reads — declarative list() slices with the firewall, the table's real mask functions, and { data } cache-seed envelopes; custom() escape-hatch sections; a SHA-1 ETag over { scopeSeed, userId, payload } with the ifNoneMatch → notModified short-circuit; and Server-Timing. Fail-closed: a bundle's roles must be a subset of every list table's read.access roles — a bundle can never widen read access. The digest ships as sha1Hex in lib/etag so hand-written reads can delete their copies. See Bundles.

Supporting changes: ActionError / TransitionLostError are re-exported from the generated .quickback/define-action helpers (routes duck-type on error.name, so project-local copies keep working); sendQueueBatched in lib/effects chunks queue sends at the Cloudflare 100-message cap; and the $record.* / $ctx.* substitution grammar used by onEnter and bundle where is one strict allowlist, always parameterized — never interpolated into SQL.

CLI device login fixed: /cli/authorize now claims the code before approving

Better Auth's deviceAuthorization plugin requires the verifying session to claim a device code (GET /device?user_code=…) before it accepts an approve or deny. The account SPA's /cli/authorize page posted the approval directly, so every quickback login failed with "Device code has not been claimed by a verifying session." The page now claims first, then approves — and surfaces claim failures (expired or mistyped codes) with the server's message.

v0.51.1 — July 16, 2026

Generated builds no longer rewrite the auth schema

Generated Better Auth projects now run tsc directly from npm run build. quickback compile already generates, qualifies, and envelopes src/auth/schema.ts; invoking auth:schema again during every build rewrote that compiler-owned artifact and made an otherwise clean release worktree dirty. The explicit auth:schema diagnostic command remains available, while normal build, Wrangler, and deployment preflight paths are source-read-only. When recompiling an existing project, compiler-owned package.json script names now converge to their current generated commands while unrelated custom scripts are preserved. This upgrades projects whose previous generated build script still invoked auth:schema.

Non-cross-tenant unsafe actions preserve delegated RLS claims

Generated Neon handlers now reserve createServiceDb for actions that explicitly enable unsafe.crossTenant. An action that enables raw database access with crossTenant: false keeps the request database, so organization and delegated-principal claims survive into Hyperdrive transactions instead of being cleared by the service-role preamble. True cross-tenant sysadmin actions retain their audited RLS bypass.

Neon Hyperdrive guidance now also calls out Postgres' SELECT … FOR UPDATE authorization rule: a locked row must pass both SELECT and UPDATE policies. Delegated actions should use ordinary selects for read-only eligibility tables and lock only mutable rows they are authorized to update. A PGlite regression proves that ordinary delegated reads remain visible while the generated restrictive update fence blocks an unauthorized row lock.

Atomic email-OTP auth routing no longer recurses on ordinary requests

The generated atomic email-OTP wrapper now captures and binds Better Auth's original request handler before replacing the public handler property. Session reads and every other non-atomic auth route therefore forward to Better Auth exactly once instead of re-entering the wrapper until the Worker exhausts its call stack. The exact email-OTP sign-in route retains its transactional admission and rollback behavior.

Generated Better Auth CLI dependencies no longer retain vulnerable Lodash

Better Auth projects now emit a root npm override for lodash@4.18.1. This keeps the supported stable auth@1.6.x CLI and its better-auth executable, while lifting the CLI's dev-only Prisma parser chain off Lodash releases affected by the 2026 code-injection and prototype-pollution advisories. Runtime Better Auth packages and generated authentication behavior are unchanged.

The CLI now merges generated npm overrides explicitly when recompiling an existing project: unrelated user overrides remain, while compiler-owned security pins win a conflict. Recompile and reinstall dependencies to refresh the project lockfile. npm audit --omit=dev remains the production dependency view; upstream development-server advisories may still appear separately.

Postgres constraint replacements apply in dependency-safe order

Neon migration post-processing now orders both halves of a Drizzle constraint replacement. A dependent foreign key is dropped before its referenced unique or primary-key constraint, then the replacement key is added before the foreign key that consumes it. This prevents Postgres 2BP01 failures when Drizzle emits the parent key drop first, while preserving byte-identical output for already-correct migrations and the relative order of unrelated statements.

Schema registry output is byte-reproducible

schema-registry.json no longer includes the wall-clock generatedAt field. Its remaining metadata and schema content are derived entirely from compiler inputs, so consecutive compiles of identical logical input now produce the same registry bytes. This makes artifact hashes, caches, and deployment diffs stable without timestamp normalization.

Generated auth and discovery surfaces now remain truthful and type-safe

Atomic email-OTP generation now imports and invokes before-email-otp-activate.ts exactly once and preserves the transaction's real Drizzle select capability in the hook context. Strict generated TypeScript therefore accepts both the base Neon service database and its interactive transaction without erasing the hook's read contract.

Contract-v2 projects that omit auth.jwt no longer advertise the disabled Quickback JWT fast path in llms.txt or RFC 8414 metadata. Bearer-JWT guidance, the fallback /api/v2/token endpoint, and JWT scopes are emitted only when the corresponding JWT surface exists; real Better Auth OAuth-provider discovery is unchanged.

Cloudflare Email callbacks now all enter the shared credential-delivery scheduler exactly once. The scheduler anchors the observed rejection with waitUntil, emits the metadata-only auth.credential_delivery_failed event, and never leaks the provider error. Generated CRUD routes also keep delegated principal and account authority disjoint: ?organizationId= is rejected for a delegated principal instead of synthesizing activeOrgId on its context. This closes both the authority ambiguity and the generated AppContext union type errors.

Email-OTP activation can be admitted atomically in the API layer

Contract-v2 Cloudflare projects using one Neon Hyperdrive database can opt the exact Better Auth POST /sign-in/email-otp handler into an interactive transaction with atomicAuthRoutes: ["emailOtpSignIn"]. The paired recognized hook, quickback/hooks/before-email-otp-activate.ts, receives a proven dormant user { userId, email, user } plus transaction-bound db / full schema and authDb / authSchema handles. Quickback owns only identity proof and atomic orchestration; application admission, status, event, pass, and eligibility rules remain authored in the project API hook.

The hook executes after OTP validation but in Better Auth's databaseHooks.user.update.before, before the user update lock. This preserves application-first lock ordering and avoids the user-to-application inversion a session-create hook would introduce. Unknown-email signup must be disabled; already-verified users do not rerun dormant activation.

The generated handler rebuilds Better Auth over the transaction handle, so OTP consumption, project hook writes, the emailVerified update, and session insert commit together. Admission denial, a non-ok response after activation starts, and any 5xx response throw a private rollback sentinel. Expected policy denial returns a generic 403. Operational rollback emits the metadata-only auth.atomic_email_otp_operational_rollback event and returns a generic 503, without exposing raw errors. Ordinary invalid-OTP 4xx responses still commit Better Auth's attempt accounting, and the OTP-send route remains outside the transaction. Unsupported contracts, runtimes, database modes, missing/duplicated config, enabled signup, and a hook/config mismatch now fail at compile time.

Neon migrations can declare cross-schema foreign keys

Neon projects can now declare fully qualified, single-column or composite foreign keys under compiler.migrations.foreignKeys. Each entry names the constraint, source and target schema/table/column tuples, and explicit onDelete / onUpdate actions. The compiler rejects unsafe identifiers, mismatched tuple arity, duplicate or ambiguous declarations, unknown keys, and non-Neon providers before generating SQL.

Declared constraints are sorted deterministically and folded into Quickback's content-addressed Postgres journal after the Drizzle schema migration. A new constraint is added NOT VALID and then validated, so existing orphan rows fail the migration; replay verifies an exact catalog match and rejects a same-named constraint with different semantics. This surface enforces relational integrity only—business rules remain in generated or authored API actions, not database triggers.

Compile completion steps honor the Neon connection mode

The CLI's successful-compile footer now distinguishes Neon HTTP from Hyperdrive. HTTP projects retain the direct DATABASE_URL Worker setup. Hyperdrive projects show DATABASE_MIGRATION_URL only as a local or CI migration input, point local development at the configured Hyperdrive localConnectionString, and never recommend deploying DATABASE_URL as a Worker secret.

Named Hyperdrive environments also show their configured Neon branches, required-secret inventories, and explicit Wrangler --env targets. The footer no longer suggests the unnamed .env.neon / .dev.vars workflow or a bare deploy for an isolated dev / prod setup.

Cloudflare email delivery and named Neon targets fail visibly and deploy explicitly

Generated Cloudflare email code now uses the native Workers SendEmail, EmailAddress, attachment, and EmailSendResult contracts. The Workers builder keeps its camel-case fields and { email, name } address shape, while the REST transport retains its separate snake-case fields and { address, name } shape. Generated projects move to @cloudflare/workers-types v5 with Wrangler 4.110 so the binding and result types agree without any casts.

Every Better Auth email callback now creates an observed delivery promise. On Workers, the callback hands that still-rejecting promise to executionCtx.waitUntil(...) and returns after the task is accepted; outside a request lifecycle it returns the promise for the caller to await. Failures emit only a structured generic event, never provider error content, and remain rejected instead of being detached or swallowed. SES and SNS background sends use the same lifecycle rule.

Neon Hyperdrive's generated postgres.js client now caps each request-scoped pool at five connections. Named environments gain explicit deploy:dev and deploy:prod package scripts (mapped to their configured Wrangler environment names), while the unsafe bare deploy script remains absent and migrations remain a separate target-owned step.

Contract v2 auth and Neon authority boundaries are fail-closed

Contract-v2 projects no longer emit Quickback's generic JWT mint endpoint, JWT helper, middleware verification/mint fast-path, or related imports unless the project explicitly declares auth.jwt. Contract v1 retains its existing default for compatibility. Adding auth.jwt: {} to a v2 config opts back into the custom Quickback JWT with its normal default settings.

Configured delegated authenticators now produce closed TypeScript unions in both generated AppContext and Neon RLS context. Request contexts contain only resolved principal variants; the compiler-internal credential-lookup variants exist only in the database context. Delegated standalone actions do not require Better Auth activeOrgId / activeTeamId before their declared principal access tree runs, and their context cannot acquire account, organization, team, membership, or scope authority.

On Cloudflare + Neon, the API-key organization membership lookup now runs with request-scoped user/org RLS claims before it stamps verified organization and role authority into AppContext. Generated security migrations also converge direct privilege drift: the audit schema/table revoke all direct grants from PUBLIC, the runtime, and admin before restoring their exact write-only/admin sets, while the Drizzle journal schema/table/sequences and database CREATE capability explicitly revoke PUBLIC and runtime access before granting the migration role.

Standalone action routes preserve their exact authority context

Generated standalone routes now pass each action the context and scoped database types inferred from that action's own execute signature. Account actions therefore receive a required account identity, while event_pass, event_delegate, and other delegated-principal actions retain their exact principal literal at the route boundary. Relationship loads / exposeAs hydration is intersected with that authority context, including required shared targets and optional distinct multi-lane targets, instead of replacing it with the broad application context.

The same boundary keeps ordinary request databases and temporary service-role-backed scoped databases assignable to the generated Hyperdrive action helper without widening the handler's schema-aware db type.

Generated principal-auth identity guards also emit explicit strict TypeScript parameter and return types. Contract-v2 Cloudflare + Neon output no longer fails tsc on implicit-any guard parameters.

Parameterized actions keep complete v2 schema harvests

The action-input harvest plan now converts Hono-style :param segments to the OpenAPI {param} form for standalone actions and parameterized resource paths. Previously the CLI could harvest every schema successfully, but the compiler could apply only parameter-free actions because it looked up parameterized operations under the wrong OpenAPI key. Contract v2 now counts and patches the same canonical path emitted by OpenAPI and MCP.

Generated standalone-action routes also remove template-only trailing spaces when optional audit blocks are absent, keeping regenerated source diffs clean.

Action-schema harvest uses the generated runtime dependency tree

The CLI now resolves action-harvest dependencies from the configured build.outputDir/node_modules before unrelated ancestor installs. This fixes isolated projects that keep authored definitions under quickback/ and the generated runtime under a sibling directory such as src/: their actions can use the runtime's Zod 4 and Drizzle installation without adding workspace-root symlinks. An older user-level or monorepo-level Zod can no longer shadow the runtime's declared version during schema evaluation.

Schema-only evaluation also emits inert named table proxies for authored Drizzle exports. Shared input/DTO modules may therefore construct top-level selection objects such as { id: records.id } without executing a database operation or losing the action's Zod schema.

Harvest failures now retain the exact action key, failing phase (bundle or evaluation), and bounded underlying error. Contract v2 includes those per-action diagnostics in its fail-closed compile error; v1 retains its best-effort static-schema fallback.

Contract v2 delegated principals and RLS-only action tables

Cloudflare + Neon Hyperdrive projects on contract.version: "v2" can now declare framework-generic delegated authenticators under auth.principals. Each named principal owns an exact, non-Bearer Authorization scheme, a project hook that proves the credential and returns only { digest }, and one compiler-owned indexed lookup that resolves the actor, optional session/family, string claims, and active status. The hook receives no database handle. Proof hooks authored under quickback/lib are staged into generated src/lib, even when no action imports them directly.

Delegated authority is structurally disjoint from Better Auth accounts: ctx.principal is populated while user, organization, team, and membership fields remain absent. Access arms may use principals: [...]; fields inside an arm are ANDed and sibling arms are ORed. Generated action helpers preserve that boundary in TypeScript, including the anonymous possibility of a PUBLIC sibling arm. Mixed credential transport rejects Authorization plus an API key or an actual Better Auth session cookie, while unrelated analytics cookies do not create a false conflict. Bearer remains reserved for account authentication.

Resources can also declare operation-specific databaseAccess policies for authored actions and support tables. This is an RLS-only surface: it emits no generic route or OpenAPI operation and is accepted only on Cloudflare + Neon Hyperdrive. Every authorization path must contain a nonempty SQL-lowerable record equality; unbounded principal/role leaves, any unbounded OR arm, empty records, ambiguous mixed boolean nodes, app-only predicates, and unsupported providers fail compilation. This keeps business logic in authored API actions while Postgres independently enforces the same event/person or account/org row boundary.

Principal claims use the same transaction-local Hyperdrive preamble as account claims, with query caching disabled. Restrictive per-operation principal fences prevent a public, exception, or legacy permissive policy from admitting the wrong principal type. Credential lookup indexes and RLS journal entries are content-addressed and converge when a table, digest column, or definition changes; generated Postgres identifiers include collision-resistant hashes and stay within the 63-byte limit. Each security migration also drops the complete compiler-owned databaseAccess policy family before recreating the current operations, so removing or moving an operation revokes the old permissive policy instead of leaving it active. Record fields resolve through authoritative Drizzle metadata to exact quoted physical identifiers, unknown fields fail the compile, and DB-only pseudo-roles follow the same ADMIN/SYSADMIN/INTERNAL contract as the application surface.

Action execute helpers preserve their typed boundary

Generated per-feature defineAction helpers no longer erase services, the Hono request context, record predicates, or the action's return value to any. Zod input and record inference remain intact, whereRecord and whereTransition accept only the helper's bound table, and the existing D1 / Neon Hyperdrive scoped db and interactive tx types now flow through the same execute boundary. The returned value is inferred exactly; this does not add a response schema, invalid-return rule, or new output contract.

HTTP/WebSocket Neon retains its intentionally dynamic db, and AppContext retains its open index signature for runtime-hydrated namespace fields.

Neon Hyperdrive is now the only Worker database lane, including Better Auth

For connectionMode: "hyperdrive", generated Better Auth code now uses the same HYPERDRIVE-backed, service-role-scoped database handle as the rest of the generated API. DATABASE_URL is no longer emitted or required as a Worker secret in Hyperdrive mode; migration credentials remain inputs to the compiler or CI environment. HTTP-mode Neon retains its existing DATABASE_URL runtime contract.

Public contract v2 now compiles as one coherent API/auth surface

Projects may set contract: { version: "v2" } to select /api/v2/* feature routes and /auth/v2/* Better Auth routes. The compiler now threads that selection through generated route mounts, Better Auth configuration and middleware, OpenAPI paths and server metadata, MCP internal tool calls, OAuth discovery, llms.txt, schema/embeddings/system routes, live views, namespace actions, and dedicated-domain /v2/* shortcuts. The option is no longer fail-closed. Omitting contract or selecting v1 preserves the existing v1 output.

Bundled CMS and Account clients now receive the selected API and auth base paths through both their Vite build environment and the Worker-injected runtime config. CMS CRUD, schema, sysadmin, and live-view requests and Account auth, data, and live-view requests therefore stay on /api/v2 and /auth/v2 for a v2 project instead of falling back to client-side v1 literals. V1 SPA config retains its historical byte shape.

V2 compiles also require a complete client-side action input-schema harvest. The current CLI reports the exact action files that failed to produce a JSON Schema, and the compiler independently rejects missing, invalid, or partial maps at the shared parse boundary used by both complete and validation-only compiles. POST /compile?complete=false can no longer bypass the v2 gate. For record actions with bulkVariant: true, the harvested schema now patches both the single-record request body and the bulk operation's nested input; the action is counted complete only when both OpenAPI targets exist. Harvest planning also normalizes filename-based action bindings to the table's declared name and resource path, so kebab-case filenames exporting snake/camel-case tables are not silently omitted. V1 remains best-effort for backward compatibility.

Complete, isolated Wrangler targets for named Neon environments

providers.database.environments now defines deployable Neon Hyperdrive targets instead of emitting empty [env.*] shells. Because Wrangler does not inherit vars or bindings into named environments, each target must explicitly own its generated vars, required-secret contract, Hyperdrive and KV IDs, rate-limit namespace IDs, and Cloudflare Email binding. The compiler rejects missing overrides, shared stateful IDs, optional secret declarations, invalid target semantics, and unsupported binding families rather than generating a Worker that starts without part of its runtime.

The required-secret inventory is now centralized and includes generated credentials as well as custom declarations: BETTER_AUTH_SECRET, a configured auth.jwt.secretEnv, ENCRYPTION_KEK when envelope encryption is present, and every bindings.secrets entry marked required. Generated Better Auth SES/SNS credentials, AWS-backed anonymous-upgrade credentials, enabled social-provider client id/secret pairs, and R2 presign credential env names are included as well. Cloudflare social auth now reads those credentials from the Worker env binding instead of relying on process.env compatibility behavior. Named targets also reject every generated top-level custom-domain source — including runtime routes, the primary/CMS/Account/Admin/Auth/API domains, and app domains or aliases — until routes can be declared with explicit per-environment ownership; top-level routes can no longer produce an ambiguous deployment target.

Named targets are currently limited to Neon Hyperdrive with Better Auth. External auth remains fail-closed until its service binding can be declared independently for every named environment.

For named-environment projects, the base Worker has no deployable stateful bindings, local development selects a named target, and that selected target is always the required logical dev environment, which must own an explicit development-only Hyperdrive localConnectionString. Quickback emits it only in the selected named binding, rejects a top-level override when named environments exist, and rejects local URLs on staging or production targets; deployed targets continue to resolve the remote Hyperdrive configuration by ID. The generated package omits the unsafe bare deploy script. Existing projects that omit environments keep byte-identical Wrangler output and the existing dev/deploy scripts. See Neon → Named Cloudflare deployment environments.

Local compiler trust boundary and deterministic Drizzle commands

The supported Docker launcher now opts into unauthenticated local compilation explicitly and publishes the compiler only on 127.0.0.1; missing or unknown compiler modes retain hosted authentication. The CLI recognizes only exact loopback hostnames as local. Migration post-commands now use the configured package manager's local binary execution form, with npm forced offline and its implicit install prompt disabled, so a missing drizzle-kit fails clearly instead of fetching an unpinned package.

v0.51.0 — July 12, 2026

Action rate limits now emit — documented since the rate-limit pillar, wired now

The rate-limit docs have described per-action rate limiting since the pillar shipped: record-based actions inherit the resource's update bucket, standalone actions inherit the project-default update bucket, and actions may declare a rateLimit override or opt out with rateLimit: false. The generated routes never contained the checks. They now do, exactly per the documented contract:

  • Record-based actions (and their bulk variants, which share the same counter — one token per batch request) are keyed <resource>:action:<name>:<user>; standalone actions are keyed standalone:<declaredPath>:<user>. The :action:/standalone: infixes mean action counters never collide with CRUD counters.
  • rateLimit: { limit, period } on an action emits its own binding (deduped by tuple, same RL_<limit>_<period> scheme); rateLimit: false opts the action out entirely.
  • Record-based action routes are now exempted from the per-resource CRUD catch-all middleware. Previously a POST /:id/approve was incidentally counted against the resource's create bucket — wrong bucket, wrong key.
  • Actions-only (tableless) projects now provision the project-default binding; previously they shipped with none.

Behavior change: standalone actions were previously unlimited; they now inherit the project-default update bucket (200 requests / 60s per user/IP with shipped defaults). PUBLIC webhook-style endpoints that absorb provider bursts should declare their own budget (rateLimit: { limit: 1000, period: 10 }) or opt out (rateLimit: false). The compiler emits a warning when a PUBLIC standalone action silently inherits the default, so affected actions are flagged at compile time.

Better Auth CLI pinned and shipped as a generated devDependency

The generated auth:schema script runs npx better-auth generate, but the better-auth runtime package ships no executable — the better-auth bin lives in the npm package auth (the official Better Auth CLI; the team took over that name at 1.5.0, superseding the frozen @better-auth/cli). Generated projects now declare auth as a devDependency pinned to the runtime's minor line, so npm run auth:schema resolves the CLI locally instead of requiring a global install. The compiler image's global CLI is pinned to the same version, eliminating silent skew between the CLI on latest and the pinned runtime.

Compile warning when an action's input schema degrades to untyped

When action input schemas aren't harvested client-side (older CLI, or a direct /compile API call), the static Zod parser supplies OpenAPI/MCP input schemas — and silently degrades unrecognized constructs (discriminated unions, refinements, z.record, transforms) to the accept-anything {}. That degradation is no longer silent: the compile result now carries a warning per affected action naming the route, source file, and the exact untyped field paths. Actions covered by the CLI harvest are never warned on, and actions that declare no input are skipped.

Auth docs: JWT security posture stated up front

The Auth & JWT page now leads with the posture summary instead of burying it: the default 180-second auto-refreshed TTL plus revocationCheck: 'kv' (seconds-level revocation, auto-stamped on ban / member removal / role change / scope-conferring row changes) is the recommended configuration and covers the vast majority of deployments — with the residual trade-offs (per-principal granularity, fail-open on KV outage, ~60s cross-PoP propagation) stated explicitly for strict threat models.

Neon via-user reads: generated list handlers now typecheck against the concretely-typed service handle

On Neon, a read firewall carrying a via-user multi-hop relationship predicate routes the generated read handlers to createServiceDb(c.env) — a genuine NeonHttpDatabase<typeof schema> — instead of the loosely-typed request db. The list handler's query construction declared drizzle select builders with let and reassigned them (query = query.where(…), .orderBy(…), .limit(…).offset(…), and the count/aggregation/group-by variants), which is a type error against drizzle's immutable builder typing: .where() returns Omit<PgSelectBase<…>, "where">, not the declared type. Every affected declaration now appends .$dynamic(), drizzle's sanctioned mode for incremental query building — types only; the emitted SQL is unchanged. Reads routed through c.get('db') were never affected at runtime (the handler code is identical); they simply never surfaced the latent typing defect because the request-db handle is not concretely typed. The Neon output tsc gate now also stages the emitted service-read route files (via-user fixture) with a control that re-strips .$dynamic() and asserts the failure, so query-builder typing regressions in generated route files fail CI.

ws-ticket minting now accepts all three authorization vocabularies

POST /broadcast/v1/ws-ticket could previously be gated only by an authz relationship role (wsTicket.role) or a namespace. The mint gate now takes an explicit discriminated access form — the arm names the vocabulary, so a bare name can never silently shadow across vocabularies:

  • access: { roles: ["member+"] }org-membership roles, with auth.roleHierarchy + expansion (compile error without a hierarchy). Firewall-consistent: the route verifies the caller's org role and that the requested scopeTable row belongs to the caller's active organization — an org-A member cannot mint a ticket to org B's room. Requires an organization column on the scope table (compile error otherwise). UPPERCASE pseudo-roles are rejected: a PUBLIC/AUTHENTICATED ticket factory would hand signed room credentials to callers with no org standing.
  • access: { authzRole: "attendee" } — the existing relationship-role gate, unchanged. The legacy role: "attendee" string remains the shorthand and compiles byte-identically (regression-pinned against a golden fixture).
  • access: { fga: { relation: "viewer", object: "event:{id}" } } — an FGA relation on the object derived from the tenant-bound scope row. The object template is the read path's access: { fga } language, and the check runs through the same org-scoped evaluator (ctx.fga.check) — not a second one. The object type and relation are validated against the authz.fga model at compile time; a non-tenant-scoped scopeTable is rejected like the namespace path.

Everything fails closed at compile time with actionable errors: exactly one authorization source (access never coexists with role or namespace), exactly one access arm, ambiguous names living in two vocabularies, missing hierarchy, missing FGA model, and missing organization columns all fail the build. See Realtime.

v0.50.8 — July 12, 2026

No changelog entry was written for this release. Reconstructed from git: audit-wrapper coverage for onConflictDoUpdate upserts (e8e208c6, #121), a Date-stamped onConflict in the Neon upsertBatch path (4fd78f77, #120), and compile-outcome telemetry (84e6c1d1).

v0.50.7 — July 12, 2026

No changelog entry was written for this release. Reconstructed from git: a version bump only, to clear an npm publish collision on 0.50.6 (b4292c05).

v0.50.6 — July 12, 2026

Neon hyperdrive: generated env.d.ts now types the HYPERDRIVE binding

connectionMode: "hyperdrive" projects emit a [[hyperdrive]] binding = "HYPERDRIVE" wrangler block and a db runtime that requires env.HYPERDRIVE.connectionString — but the generated src/env.d.ts omitted the binding, so every generated createDb(c.env) / createServiceDb(c.env) call site (middleware, routes, webhooks, seal routes) failed tsc with Property 'HYPERDRIVE' is missing in type 'CloudflareBindings'. The Env interface now declares HYPERDRIVE: Hyperdrive (the @cloudflare/workers-types global) whenever the resolved Neon connection mode is hyperdrive. The wrangler-binding completeness matrix gained neon http/hyperdrive cases pinning env.d.tswrangler.toml agreement, and a new test typechecks the real emitted env/db files with the exact generated call shape.

Neon hyperdrive: typed action db accepts explicit values for defaulted columns

On the hyperdrive typed action surface, db.insert(table).values({ id: ..., ... }) failed tsc with 'id' does not exist in type 'QbPgScopedInsertValue<…>' for any column with a schema default — explicit ids on $defaultFn primary keys, explicit status on defaulted enums, explicit audit timestamps. The scoped insert model was derived from drizzle's PgInsertValue<TTable>, which drops every optional (defaulted) key when instantiated through a generic type parameter under TypeScript 5.9. It is now derived from the table's own $inferInsert (which survives generic instantiation), preserving optionality and drizzle's per-column SQL | Placeholder escape hatch. Required columns, unknown-column rejection, and the auto-scope-column relaxation (organizationId/ownerId/teamId optional) are unchanged. Note: pg timestamp columns are Date-typed — passing new Date().toISOString() (the D1 string convention) into a typed insert or update still fails tsc, matching stock drizzle semantics; pass a Date.

Production 500 bodies are now generic — error internals stay in the logs

Unhandled-error responses (INTERNAL_ERROR from the global onError, ACTION_EXECUTION_FAILED from action handlers) previously forwarded the underlying error message, cause-chain message, and the top three stack frames in the response body by default. Those can carry SQL fragments, file paths, or secrets, so the default body is now generic: { error, code, layer, hint, requestId } plus details.name (error class only) on action failures. The full error — message, cause, stack — is always console.error'd keyed by the requestId echoed in the body. Setting EXPOSE_ERROR_STACK=1 (dev/staging) restores the verbose body: message, details.cause, sanitized details.frames, and the full details.stack. See Errors.

wrangler.toml now declares every binding the generated code references

Two configurations generated code referencing a binding that wrangler.toml never declared — the env key was undefined at runtime and the first use 500s:

  • Resource-level realtime (realtime: { enabled: true } on a table, without the top-level realtime: true database flag) emitted the realtime routes, the BROADCASTER env type, and the worker's Broadcaster DO export, but no [[durable_objects.bindings]] / [[migrations]] block — every broadcast failed at runtime. The wrangler emitter now derives realtime from the same resolved surface as the rest of the compiler (top-level flag OR any resource opt-in), so the DO binding and migration are always emitted together with the code that uses them.
  • splitDatabases: false + webhooks dropped the WEBHOOKS_DB [[d1_databases]] block that the generated webhooks lib reads. Single-db projects with webhooks now get the block, matching split mode.

A compile-level test matrix (split on/off × files × webhooks × realtime) now pins binding uniqueness and completeness: every D1Database / DurableObjectNamespace binding typed in src/env.d.ts appears in wrangler.toml exactly once.

v0.50.5 — July 12, 2026

No changelog entry was written for this release. Reconstructed from git: a Neon role-bootstrap fix for fresh PG16+ deploys — GRANT quickback_owner … WITH SET (ff175ccc). The rest of the window is quickback start funnel work that does not affect compiled output.

v0.50.4 — July 12, 2026

Security: three fail-closed fixes in the generated runtime

  • Scoped db blocks Drizzle's relational query API. db.query.<table>.findFirst() / findMany() build their SQL outside the scoped wrapper's reach, so they read across organizations. The scoped db handed to actions now throws on any db.query.<table> access (and the Neon hyperdrive typed db/tx fails tsc on it) with directions to db.select() (auto-scoped) or an unsafe: true action's unsafeDb.query with an explicit scope predicate. Handlers that relied on the unscoped pass-through were reading other tenants' rows and must be updated. See Scoped relational-query safety.
  • POST /api/v1/token requires session authentication. The auth middleware now stamps ctx.authMethod ('session' | 'jwt' | 'api-key' | 'oauth'), and the token endpoint rejects anything but 'session' with 401 AUTH_SESSION_REQUIRED. Pre-fix, a JWT could mint its own replacement — a stolen or revoked token (banned user, removed member, demoted role) could roll a 180s-TTL token forever without re-validating the session.
  • Broadcaster DO no longer trusts the X-Internal-Call header alone. Inline-mode checkAuth now requires X-Internal-Secret to match BETTER_AUTH_SECRET (already provisioned; the secret never leaves the worker/DO boundary on a stub call), separate mode compares ACCESS_TOKEN constant-time, and every generated internal caller (lib/realtime.ts senders, the live-view seq reader, the Better Auth role-change hook) sends the secret. A spoofed header can no longer reach broadcast injection or webhook repointing through any forwarded route.

v0.50.3 — July 11, 2026

Neon reads through user-anchored relationships

Resources readable via a multi-hop chain anchored on the caller's user id (e.g. a conference attendee reaching their own registrations through person_account_links → people → event_people → registrations) now work on Neon. Declare the chain once with the hops form on an authz.relationships entry and reference it from a resource firewall via { field, via }. Because the correlated multi-hop join reaches across intermediate tables that each FORCE ROW LEVEL SECURITY, caller-claims RLS would deny such a read to zero rows — so the compiler routes only that resource's read handlers (collection GET /, GET /:id, and views) through the service-role handle (createServiceDb), with the compiled app-layer reachability WHERE as the sole row filter. Org/owner/team reads are untouched (caller claims + RLS backstop); writes are unchanged. The swap is fail-closed — a service-handle read with no firewall WHERE is a compile error. The anchor table is required to be caller-read-own on the subject column and service-role-write-only (a forgeable link would mint reachability to a victim's data); this link-integrity check is target-independent and fails the compile on both Neon and D1. The same config compiles identically on D1, where the app-layer WHERE was already the whole firewall. See Reads through a user-anchored relationship.

v0.50.2 — July 11, 2026

Neon interactive transactions via Hyperdrive

A Neon project can now opt into connectionMode: 'hyperdrive' to run its feature database over a Cloudflare Hyperdrive binding (postgres.js / drizzle-orm/postgres-js), unlocking real interactive transactions in generated actions: read current state, lock it (for update), decide in TypeScript, then write dependent rows — committed atomically, rolled back on throw. tx is fully schema-typed (invalid columns fail tsc), the verified RLS claims run as the first transaction-local statement inside the transaction (no leak across the connection pool), and Hyperdrive query caching is force-disabled (ETags cache at the correct post-auth layer). http mode stays the lightweight batch-only default and is byte-identical; requesting db.transaction() outside hyperdrive mode is a fail-closed compile error. WebSocket mode is deprecated — Hyperdrive supersedes it as the full-capability path. See Interactive transactions.

Neon migration & environment tooling

environments config maps dev/staging/prod to Neon branches + Cloudflare envs; quickback canary <env> branches the target DB, applies pending migrations to the branch, diffs, and (with --apply) promotes — verifying schema changes against real state before they touch a live environment. Record-condition firewall predicates now lower to Postgres RLS policy arms (enforced twice — app layer and database). Opt-in API contract.version scaffolding landed (v1 default; v2 fail-closed pending completion).

v0.50.1 — July 10, 2026

No changelog entry was written for this release. Reconstructed from git: Neon driver 1.1 compatibility (claim-scoped SQL wrapper, 9a3e82a5) and migration-journal hardening — admin-runnable Drizzle journal, UNIQUE-before-FK ordering (88697e0b).

v0.50.0 — July 10, 2026

Neon PostgreSQL reaches deploy parity with D1

A Neon-backed service now compiles, typechecks, migrates, and deploys with the same capability surface as a D1 service — over HTTP (@neondatabase/serverless), the Cloudflare default. There is no Hyperdrive binding and no Neon Authorize / JWKS configuration: the Worker verifies each caller, then writes the trusted user/org/team context into transaction-local Postgres settings (set_config('request.jwt.claim.sub' | 'org_id' | 'team_id', …, true)) as the first statement of every query batch. RLS reads them through a compiler-defined auth.user_id() shim and the get_active_org_id() helper — not a persisted user_sessions mirror (which never shipped).

  • Background contexts run under a service-role handle. Queue consumers, cron schedules, and cross-tenant unsafe actions acquire createServiceDb(env), which sets quickback.service_role = 'true' (all request claims cleared); every feature table carries a <table>_service_role RLS policy admitting exactly that context. Unsafe-action audit events are unchanged.
  • Webhooks port to a webhooks Postgres schema in the same database (no WEBHOOKS_DB binding). webhooksBinding is a pure enable flag on Neon; the D1-only webhooksDatabaseId / webhooksDatabaseName keys are rejected. The store is service-role-only.
  • .encrypted() / .sealed() columns are supported on Neon over HTTP: the key/vault tables fold into the journaled migration set with org-scoped and service-role-only RLS.
  • Batch transactions advertise the truth. Neon HTTP is fail-fast and non-transactional (meta.transactional: false, like D1) because drizzle-orm/neon-http has no interactive transactions; Neon WebSocket (Node/Bun) gets real rollback (meta.transactional: true).
  • Deploy glue matches D1: a generated deploy script (npm run db:migrate && wrangler deploy), a boot check asserting DATABASE_URL, .dev.vars.example (wrangler dev reads .dev.vars, not .env) gitignored, and drizzle.config.ts preferring DATABASE_MIGRATION_URL ?? DATABASE_URL for the privileged migration role.
  • Fail-closed capability gates: managed file storage (use presign-only R2), the Better Auth subscriptions plugin, raw sqliteTable interop sources on a Postgres target, and — under WebSocket mode — webhooks / unsafe actions / encrypted columns are all compile-time errors rather than silently-broken output. Mixed database providers remain unsupported.
  • Docs corrected: the Neon page no longer describes Neon Authorize, a user_sessions table, or Hyperdrive — none of which the compiler emits.
  • Migration-journal hardening. The role bootstrap now grants quickback_admin everything the pinned Drizzle runner's journal needs — database-level CREATE (so CREATE SCHEMA IF NOT EXISTS drizzle passes even as a no-op) plus ownership of the drizzle schema and drizzle.__drizzle_migrations — so routine migrates after the owner-run bootstrap need no owner credential. Generated Postgres migrations are also post-processed so a referenced composite UNIQUE / PRIMARY KEY constraint always precedes the foreign keys that consume it (an upgrade delta applies unedited instead of failing with "no unique constraint matching given keys").

On this page

v0.54.0 — July 17, 2026Realtime: named invalidationsAccess: SESSION and SCOPED pseudo-role tiersAccess: fail-closed scope/firewall coherence guardrailv0.53.0 — July 17, 2026Realtime: the broadcast surface tracks contract.versionWeb Push: VAPID delivery, queue fan-out, and identity-tag targetingv0.52.1 — July 17, 2026CLI: cold-start tolerance for the cloud compilerv0.52.0 — July 17, 2026Five action primitives: afterCommit, realtime emit, transitions v2, refs, bundlesCLI device login fixed: /cli/authorize now claims the code before approvingv0.51.1 — July 16, 2026Generated builds no longer rewrite the auth schemaNon-cross-tenant unsafe actions preserve delegated RLS claimsAtomic email-OTP auth routing no longer recurses on ordinary requestsGenerated Better Auth CLI dependencies no longer retain vulnerable LodashPostgres constraint replacements apply in dependency-safe orderSchema registry output is byte-reproducibleGenerated auth and discovery surfaces now remain truthful and type-safeEmail-OTP activation can be admitted atomically in the API layerNeon migrations can declare cross-schema foreign keysCompile completion steps honor the Neon connection modeCloudflare email delivery and named Neon targets fail visibly and deploy explicitlyContract v2 auth and Neon authority boundaries are fail-closedStandalone action routes preserve their exact authority contextParameterized actions keep complete v2 schema harvestsAction-schema harvest uses the generated runtime dependency treeContract v2 delegated principals and RLS-only action tablesAction execute helpers preserve their typed boundaryNeon Hyperdrive is now the only Worker database lane, including Better AuthPublic contract v2 now compiles as one coherent API/auth surfaceComplete, isolated Wrangler targets for named Neon environmentsLocal compiler trust boundary and deterministic Drizzle commandsv0.51.0 — July 12, 2026Action rate limits now emit — documented since the rate-limit pillar, wired nowBetter Auth CLI pinned and shipped as a generated devDependencyCompile warning when an action's input schema degrades to untypedAuth docs: JWT security posture stated up frontNeon via-user reads: generated list handlers now typecheck against the concretely-typed service handlews-ticket minting now accepts all three authorization vocabulariesv0.50.8 — July 12, 2026v0.50.7 — July 12, 2026v0.50.6 — July 12, 2026Neon hyperdrive: generated env.d.ts now types the HYPERDRIVE bindingNeon hyperdrive: typed action db accepts explicit values for defaulted columnsProduction 500 bodies are now generic — error internals stay in the logswrangler.toml now declares every binding the generated code referencesv0.50.5 — July 12, 2026v0.50.4 — July 12, 2026Security: three fail-closed fixes in the generated runtimev0.50.3 — July 11, 2026Neon reads through user-anchored relationshipsv0.50.2 — July 11, 2026Neon interactive transactions via HyperdriveNeon migration & environment toolingv0.50.1 — July 10, 2026v0.50.0 — July 10, 2026Neon PostgreSQL reaches deploy parity with D1