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/v1everywhere, byte-identical to prior output. - contract v2 →
/broadcast/v2everywhere, 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 path —
enqueuePushFanout(queue, target, payload)from an action's after-commit phase (effects.enqueue), so rolled-back writes never notify anddryRunsends 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-readpush_subscriptionswith aviews.minedevice list. Creates validatehttps:endpoints (SSRF blocker), 65-byte P-256p256dh, 16-byteauth;tagsis 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
vapidPrivateKeyin config is a compile error (env:VAPID_PRIVATE_KEYonly); the worker refuses to boot without the three VAPID vars; the private key never lands inwrangler.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_KEYandwindow.__QUICKBACK_RUNTIME.vapidPublicKey. - CLI —
quickback push generate-keys [--out <file>];--outwrites the private key to a0600file 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.
-
afterCommithooks +effects.enqueue— named best-effort side effects that run afterexecutereturns. Each hook is independently wrapped (failures log as[<action>] afterCommit:<hook> failed, never touching the response), non-blocking hooks rideexecutionCtx.waitUntil,{ blocking: true }awaits before responding, and everything skips whenexecutethrows or returnsdryRun: true. The execute context also gainseffects.enqueue(name, fn)for mid-execute captures andnow— one ISO timestamp per invocation. Replaces the per-legtry/catch + waitUntilidiom. 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 declareemit:for a pre-boundemitCrud(row, oldRow?). Updates/deletes pre-select old rows on the same WHERE; rawunsafeDbwrites stay invisible. See When auto-broadcast fires. -
Transitions v2 —
transitiongrowsguard(equality / null / notNull /custom(record)),onIllegal { code, status, message },idempotent: "noop", andstamp/clearsthat ride a compiler-applied, TOCTOU-safe UPDATE (guards fold into the WHERE; a lost race is 409ACCESS_TRANSITION_LOST). With an applied write,executebecomes optional. Table-leveltransitions: { publish: { field, from, to, access } }generates the action files — includingundo:inverses andonEntercascades — 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 onePromise.all, assertmatchScope/ cross-inputmatchWithparenthood, and inject rows under theirasnames. Ref keys are validated against the inlinez.objectinput (with agreeing optionality) at compile time. See Refs. -
defineBundle— single-round-trip bootstrap reads — declarativelist()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 theifNoneMatch → notModifiedshort-circuit; andServer-Timing. Fail-closed: a bundle's roles must be a subset of every list table'sread.accessroles — a bundle can never widen read access. The digest ships assha1Hexinlib/etagso 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 keyedstandalone:<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, sameRL_<limit>_<period>scheme);rateLimit: falseopts the action out entirely.- Record-based action routes are now exempted from the per-resource CRUD
catch-all middleware. Previously a
POST /:id/approvewas incidentally counted against the resource'screatebucket — 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, withauth.roleHierarchy+expansion (compile error without a hierarchy). Firewall-consistent: the route verifies the caller's org role and that the requestedscopeTablerow 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: aPUBLIC/AUTHENTICATEDticket factory would hand signed room credentials to callers with no org standing.access: { authzRole: "attendee" }— the existing relationship-role gate, unchanged. The legacyrole: "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'saccess: { 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 theauthz.fgamodel at compile time; a non-tenant-scopedscopeTableis 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
onConflictDoUpdateupserts (e8e208c6, #121), aDate-stampedonConflictin the NeonupsertBatchpath (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.ts ↔ wrangler.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-levelrealtime: truedatabase flag) emitted the realtime routes, theBROADCASTERenv type, and the worker'sBroadcasterDO 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 theWEBHOOKS_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 isquickback startfunnel work that does not affect compiled output.
v0.50.4 — July 12, 2026
Security: three fail-closed fixes in the generated runtime
- Scoped
dbblocks 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 scopeddbhanded to actions now throws on anydb.query.<table>access (and the Neon hyperdrive typeddb/txfailstscon it) with directions todb.select()(auto-scoped) or anunsafe: trueaction'sunsafeDb.querywith 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/tokenrequires session authentication. The auth middleware now stampsctx.authMethod('session' | 'jwt' | 'api-key' | 'oauth'), and the token endpoint rejects anything but'session'with401 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-Callheader alone. Inline-modecheckAuthnow requiresX-Internal-Secretto matchBETTER_AUTH_SECRET(already provisioned; the secret never leaves the worker/DO boundary on a stub call), separate mode comparesACCESS_TOKENconstant-time, and every generated internal caller (lib/realtime.tssenders, 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
unsafeactions acquirecreateServiceDb(env), which setsquickback.service_role = 'true'(all request claims cleared); every feature table carries a<table>_service_roleRLS policy admitting exactly that context. Unsafe-action audit events are unchanged. - Webhooks port to a
webhooksPostgres schema in the same database (noWEBHOOKS_DBbinding).webhooksBindingis a pure enable flag on Neon; the D1-onlywebhooksDatabaseId/webhooksDatabaseNamekeys 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) becausedrizzle-orm/neon-httphas no interactive transactions; Neon WebSocket (Node/Bun) gets real rollback (meta.transactional: true). - Deploy glue matches D1: a generated
deployscript (npm run db:migrate && wrangler deploy), a boot check assertingDATABASE_URL,.dev.vars.example(wrangler dev reads.dev.vars, not.env) gitignored, anddrizzle.config.tspreferringDATABASE_MIGRATION_URL ?? DATABASE_URLfor the privileged migration role. - Fail-closed capability gates: managed file storage (use presign-only R2),
the Better Auth
subscriptionsplugin, rawsqliteTableinterop 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_sessionstable, or Hyperdrive — none of which the compiler emits. - Migration-journal hardening. The role bootstrap now grants
quickback_admineverything the pinned Drizzle runner's journal needs — database-levelCREATE(soCREATE SCHEMA IF NOT EXISTS drizzlepasses even as a no-op) plus ownership of thedrizzleschema anddrizzle.__drizzle_migrations— so routine migrates after the owner-run bootstrap need no owner credential. Generated Postgres migrations are also post-processed so a referenced compositeUNIQUE/PRIMARY KEYconstraint always precedes the foreign keys that consume it (an upgrade delta applies unedited instead of failing with "no unique constraint matching given keys").