# v0.50 – v0.54

## 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](/platform/realtime/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](/define/access).

### 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 path** — `enqueuePushFanout(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`.
- **CLI** — `quickback 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](/platform/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](/define/triggers#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](/platform/realtime/durable-objects#when-auto-broadcast-fires-and-when-you-call-it-manually).

- **Transitions v2** — `transition` 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](/define/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](/define/actions/defining#refs-scoped-foreign-key-inputs).

- **`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](/define/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](/platform/database/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](/define/rate-limit#actions) 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](/configure/auth) 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](/platform/realtime#enabling-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.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](/api/errors#exposing-error-details-in-dev--staging).

### 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](/define/actions/scoped-db#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](/platform/database/neon#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](/platform/database/neon#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](/platform/database/neon)
  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").

