# Changelog

The current release series. Older entries are archived by version range:

- [v0.50 – v0.54](/changelog/v0.5x)
- [v0.40 – v0.48](/changelog/v0.4x)
- [v0.32 – v0.39](/changelog/v0.3x)
- [v0.20 – v0.29](/changelog/v0.2x)
- [v0.18 and earlier](/changelog/v0.1x)

## Unreleased — the standard API shape

### BREAKING: `contract.version` is retired

There is one standard API shape, so there is nothing to select. Pinning
`contract.version` is now a **compile error** on either value.

**Delete that one line and keep `contract.routes` if you have it.** `routes`
is the surviving axis and is what decides whether your URLs read `/api/v1` or
`/api/v2` — removing it would move them.

{/* doc-compile: skip — config fragment, not a compilable project */}
```typescript
contract: { version: "v2", routes: "v1" }   // before
contract: { routes: "v1" }                  // after — same URLs, same behavior
```

Deleting `version: "v2"` changes **no** behavior: it selected what is now
simply the standard shape. Deleting `version: "v1"` is a real migration — see
[the contract page](/platform/api-contract#contractversion-is-retired) for the
four behaviors that pin opted out of, all now unconditional.

### BREAKING: CloudEvents `type` is reverse-DNS prefixed

Realtime `postgres_changes` and `view_changes` frames now carry a
`dev.quickback.` prefix on the CloudEvents `type`:

```
myapp.registrations.insert        →  dev.quickback.myapp.registrations.insert
myapp.view.agendaBoard.changed    →  dev.quickback.myapp.view.agendaBoard.changed
```

**`qbframe` is the supported discriminator — do not match on `type`.** Its
shape is not a stable contract; `qbframe` (`"broadcast"` / `"postgres_changes"`
/ `"view_changes"`) is. A client switching on `type` breaks here.

**Author-supplied broadcast event names are unchanged.** A named-invalidation
frame's `type` is still your `wireName` verbatim — that vocabulary is yours,
not ours to prefix.

`/asyncapi.json` documents the new `type` patterns, and its `info.version` is
now the event-document version rather than a contract version.

### BREAKING: outbound webhooks are Standard Webhooks only

The legacy `X-Webhook-Signature` / `X-Webhook-Event` / `X-Webhook-Delivery`
headers and the Stripe-format `signPayload` / `verifySignature` pair are gone
from the generated project — including from the public webhooks module's
exports. Receivers must verify with `webhook-signature`. This is the one
change a **receiver** absorbs without any config change of its own, so tell
your endpoint partners.

### BREAKING: `include` and `fields[...]` are reserved query params

`?include=` is always available now (where allowlisted), which makes `include`
a reserved parameter and any `fields[`-prefixed key reserved too. **A column
literally named `include` is no longer filterable** via `?include=…`. Unknown
`?fields=` and `fields[<fk>]=` names return **400** instead of being dropped.

### Fixed: scoped projects without `auth.jwt` could not admit their own scope tokens

Scope admission used to be emitted only inside the JWT credential lane, so a
project with `authz.scopes` and no `auth.jwt` minted scope tokens that no
generated code could accept — 401 on every presentation, with no diagnostic.
Live since v0.64.0, when that lane stopped being on by default.

Admission is now emitted whenever the project declares scopes, independent of
`auth.jwt`. **No config change is needed** — and declaring scopes does not
open the credential surface: no `POST /token` route, no identity fast-path,
no bearer fast-path unless you separately declare `auth.jwt`.

This covers **sessionless** scope tokens (`ctx.mintScope`). A token minted
from a session — `POST /scope/v1/enter`, or a namespace with
`mintScope: true` — carries user claims rather than the scope-only marker, so
without `auth.jwt` it still cannot be admitted: 200 from the mint, 401 on
presentation. The compiler now warns instead of failing silently. Declare
`auth: { jwt: {} }` if you use those routes.

### CMS and Account consume CloudEvents frames as-is

The bundled SPAs no longer invert realtime frames back into a private
`{ type, table, event }` shape, and they no longer sniff `/api/v2` to pick
the broadcast URL. They match on `qbframe`, read `data` / `qbseq`, and take
`/broadcast/v1` or `/broadcast/v2` from the injected runtime config — the
same prefixes the worker actually mounts.

## v0.65.x — August 16, 2026

### Passkeys no longer silently degrade to `rpID: 'localhost'`

The generated passkey plugin (and the SEALED reveal step-up, which must verify
against the same relying party) now bakes a compile-time fallback derived from
your configured domain — `account.domain` when set, else the unified host.
`ACCOUNT_URL` remains a runtime override, and its **bare origin** is now used
for WebAuthn even when the value carries a path (`https://host/account`), so
one variable serves both the email link base and the relying-party origin.
The rpID derivation also stops stripping two-label hostnames to a bare TLD
(`example.com` no longer became rpID `com`). `localhost` now applies only to
projects with no configured domain at all.

### `APP_URL` — per-deploy override for `account.appUrl`

`account.appUrl` is baked at compile time into the Account SPA runtime config
and the "Go To App" email CTAs, which pointed preview/dev deploys at
production. Set `APP_URL` on the deploy target to override both at serve
time — same one-bundle-several-hostnames pattern as `EXTRA_APP_HOSTS` and
`BETTER_AUTH_URL`.

### Account SPA keeps its `/account` basepath through login redirects

On path-mounted deployments, a post-login redirect could navigate to a bare
path (`/dashboard` instead of `/account/dashboard`) and fall through to a
tenant route. The callback sanitizer now re-applies the basepath to
same-origin paths, covering every sign-in flow (password, passkey, email OTP,
signup) and social-link callbacks.

## v0.65.0 — August 16, 2026

### All Quickback auth plugins are now bundled — zero `@quickback-dev/*` npm dependencies

The compiler now vendors the AWS SES, AWS SNS, combo-auth, and
upgrade-anonymous plugin sources into generated projects at
`src/plugins/<name>/`, completing what the subscriptions plugin started in
v0.64: **no generated project installs a `@quickback-dev/*` package from npm.**
The plugin code ships with the compiler that wires it, so the emitted callers
(`getSESConfig`, OTP callbacks, `upgradeAnonymous()` calls) can never skew from
the plugin version they were generated against — and upgrades arrive by
recompiling, not by `npm update`.

Recompile and the dependencies disappear from `package.json`; no config
changes needed. The `@quickback-dev/better-auth-*` npm packages are being
retired from the registry — the vendored source in your project is the
supported copy.

## v0.64.0 — August 15, 2026

### `contract.version` defaults to v2 — and no longer moves your URLs

**Semantics and routes are now independent axes.**

| Key | Controls | Default |
|---|---|---|
| `version` | Framing, strictness, the auth lane | `'v2'` |
| `routes` | The URL prefix, nothing else | `'v1'` |

A project with no `contract` block runs the **v2 contract on `/api/v1`**. You
inherit the flagship semantics; your clients keep calling the address they
already call.

Previously these were one knob: `version: 'v2'` moved `/api`, `/auth`, and
`/broadcast` to `/v2` as a side effect of selecting v2 behavior. That coupling
made the better contract expensive to adopt for the wrong reason — a URL
migration nobody asked for.

**Breaking — action required if you pinned `version: 'v2'` for the URLs.**
Add `routes: 'v2'` to keep serving `/api/v2`:

```typescript
contract: { version: "v2", routes: "v2" }
```

Without it, a previously-v2 project moves back to `/api/v1` on its next
compile. Semantics are unaffected either way.

### What the v2 default changes on the wire

URLs do not move. These do:

- Realtime frames become CloudEvents 1.0 envelopes. The largest change — every
  subscriber re-parses. Routing info moves to `type`, payload to `data`.
- Outbound webhooks are signed Standard-Webhooks-only; the legacy
  `X-Webhook-*` headers stop being sent. v1 already sent **both** sets, so
  consumers can migrate to `webhook-signature` before upgrading and the flip
  becomes a no-op for them.
- Unknown `?fields=` names return `400` instead of being silently dropped.
- The Quickback HMAC-JWT lane is off unless `auth.jwt` is declared.
- `?include=`, aggregate changesets (`owns`), and delegated principals become
  available without a pin.

Pin `contract: { version: "v1" }` to opt out. A pre-v0.64 project that leaves
`contract.version` unset gets a one-time notice naming what changed — the
generated `wrangler.toml` header records the resolved version in a
`# quickback:contract` stamp, so new projects and projects that have already
compiled on v2 stay warning-free without restating the default.

### Action-schema harvest no longer keys on contract version

`assertCompleteActionInputSchemas` fired whenever the contract was v2 and any
harvested schema was absent. With v2 as the default that would have failed
every [Start](https://start.quickback.dev) project containing an action —
`rawFiles` callers post project source and never execute action code, so they
cannot harvest at all.

The gate now fires only on a **partial** payload: some schemas supplied,
others missing. Callers that supply none get the coverage warning instead. The
CLI has failed closed on its own since v0.63.0, with the local per-action
reason the compiler cannot know.

### Fixes

- CloudEvents framing was derived from `broadcastBase.endsWith('/v2')`. Once
  routes and semantics decoupled, that silently reverted every default project
  to raw v1 frames. Framing is now passed explicitly.
- The bundled CMS/Account SPAs gated their base-path override on *semantics*
  when it is a *routes* concern, emitting a redundant `/api/v1` override for
  every new project.

## v0.63.0 — August 14, 2026

### Action input schemas now fail the compile instead of degrading

**Affects every project with actions, on every contract version.**

The CLI harvests each action's `input` locally with `z.toJSONSchema`. When an
action failed to harvest, the compiler fell back to a static parser that reads
Zod as text — and that parser cannot emit `required` or `additionalProperties`.
The resulting operation kept its `properties` and looked fully typed in
`openapi.json` and the MCP tool list while enforcing nothing: generated clients
treated required fields as optional and unknown keys passed. Constructs the
parser could not read at all degraded further, to a bare `{}`.

None of it was visible in a diff. One project shipped ~346 unenforced request
schemas this way; the change read as reformatting.

A failed harvest is now a compile error naming each action, whether bundling or
evaluation failed, and the underlying local error. This was previously the
behavior of `contract.version: 'v2'` only.

`quickback compile --allow-degraded-schemas` restores degrade-and-continue for a
single compile. It prints every action that fell back and why.

The compiler also warns when a caller supplies no schemas at all — `rawFiles`
callers such as the Start app cannot harvest — naming how many actions of the
total lost `required`, with sampled paths.

### `contract.version` default flips to v2 in v0.64.0

An unset `contract.version` resolves to `'v1'` today. That default becomes
`'v2'` in **v0.64.0**. See the
[contract matrix](/platform/api-contract#contractversion-is-retired).

> v0.64.0 shipped this **without** moving any URLs — semantics and routes became
> independent axes, so `/api/v1` stays put. This entry originally said the flip
> would move `/api/v1` → `/api/v2`; that is not what happened.


Every compile that leaves `contract.version` unset now warns. Pin
`contract: { version: "v1" }` to keep the current wire, or set `"v2"` to migrate
now.

**Pinning your CLI version does not pin the contract.** The default lives in the
compiler, which is a hosted service, so an older CLI inherits the flip too. The
config key is the only pin.

### MCP tool grouping no longer assumes a `v1` path segment

Internal. Group derivation matched the literal segment `v1`, so a contract-v2
path would have fallen through and grouped under `api`. Operation tags take
precedence and every generated operation carries one, so no shipped tool list
was affected — the fallback is now correct for any `/api/v<n>/` prefix ahead of
the v0.64.0 default flip.

## v0.62.0 — August 13, 2026

### Security: `record:` access arms on `GET /:id` were never evaluated

**Affects any resource whose `read.access` contains a `record:` arm.**

A code rewrite corrupted `"$ctx.userId"` into `"$effectiveCtx.userId"` inside the
serialized access tree, and only the `$ctx.` prefix is recognised at runtime. The
arm therefore compared a column against the literal string and was permanently
false: a caller the config named was denied with 403 on the single-record read,
while the same tree admitted them everywhere else.

Those arms now evaluate. **Callers your config grants access to will start
receiving 200 where they previously received 403.** That is the documented
behavior being restored, not a widening — no caller is admitted whom the tree
does not name. Durable Object room gates already behaved correctly, so this also
removes a divergence where a room admitted a caller that `GET /:id` refused.

### Security: revocation is per scope kind, not per token

**Affects projects using `authz.scopes`.**

A token carried one proof timestamp for a claim map whose kinds were proven at
different moments, so re-proving one kind restamped the others. A revoked kind
survived indefinitely as long as the caller kept re-proving any sibling.

Each entry now carries its own proof time, and both readers use it: the
revocation check, and the carry-forward cap. **The cap matters more** — it
applies on the default configuration, where `revocationCheck` is unset and no
KV marker is ever read, so it was the only bound on a carried claim.

Tokens minted before this release have no per-entry stamp and fall back to the
token-level proof time, conservatively. `authz.scopes[].roles[].subKeys` may no
longer declare `id`, `roles`, `subjectId`, or `sctMs` — those are server-minted,
and a row value overwriting one would forge a proof time. Compile fails naming
the path.

### Generated projects with in-worker Durable Objects can install again

`npm install` failed with `ERESOLVE` immediately after a successful compile:
`hono-party` requires `@cloudflare/workers-types@^4`, and Quickback pins `^5`.
Unsatisfiable at any version — no `hono-party` release accepts 5.x.

Generated `package.json` now carries a scoped override so the peer resolves to
the project's own pin. Nothing outside `hono-party` is relaxed. Only projects
declaring an in-worker Durable Object receive it.

### Fixed: two configurations that emitted uncompilable projects

- `plugins: { apiKey: false }` emitted a `problemJson(...)` call without its
  import, so the generated project failed `tsc`.
- `organization: { allowUserToCreateOrganization: … }` in object form emitted the
  key twice in one object literal (`TS1117`). `organizationHooks` had the same
  collision and is now stripped from the merged options — the compiler owns it.

### `defineEmail` — inbound mail, provider-agnostic execute

Declare an inbound handler in `services/email/*.ts`. The compiler emits a shared `deliverInboundEmail()` dispatcher plus a Cloudflare Worker `email()` adapter. **Execute sees a portable `InboundEmailMessage`** (`from`, `to`, `headers`, `raw: ArrayBuffer`) — the same body runs for Cloudflare Email Routing and Amazon SES (Receipt → S3 → SNS calling `deliverInboundEmail`). Optional `to` matches the envelope recipient. Unmatched messages log a warning and are **not** rejected. Audit actor is `system:email-<name>`. See [Email](/platform/email).

## v0.61.0 — August 12, 2026

### Security: JWT revocation now fails closed when KV is unreachable

**Affects projects using `auth.jwt.revocationCheck: 'kv'`** — including every
project with a `.sealed()` column, which force-enables it.

Setting `revocationCheck: 'kv'` asks the verifier to consult current security
state before trusting a signed bearer. It did the opposite under failure: a
missing `KV` binding, a failed read, or a corrupt entry all returned "not
revoked," so a token was accepted during exactly the outage that hid its
revocation. A banned user's bearer kept working for the rest of its TTL, and
the one signal that would have caught it was the thing that had broken.

The check now returns `active` / `revoked` / `unavailable`, and only `active`
authenticates the bearer. A corrupt stamp is `unavailable` too — it used to
parse as `NaN` and compare as "not revoked."

**What changes operationally.** A bearer whose revocation state can't be read
gets a retryable `503 AUTH_REVOCATION_UNAVAILABLE` ([RFC 9457](/api/errors))
instead of being let through. Recovery is a separate cookie-only request: a
request carrying `Authorization` has its cookie stripped before session auth,
so during a KV outage every JWT-bearer request gets the 503 until the client
retries on its session cookie. One reason-only log line per occurrence carries
the cause (`missing-binding` / `read-error`) with no token, claim, or KV key in
it — **alert on it.**

This ties bearer-token availability to KV. That is the trade the opt-in now
makes honestly, and it is the right way round: a revocation check that gives
up under load is worth nothing. If you want the TTL bound *without* the
coupling, leave `revocationCheck` at `'none'` and set `expiresIn: 30`.

Because the binding is now load-bearing, a project with named `environments`
must declare `bindings.kv` on **every** target — a compile error naming the
environment, rather than a 503 discovered after deploy.

`revocationCheck: 'none'` output is byte-identical. See
[`revocationCheck`](/configure/auth#revocationcheck-none--kv--token-revocation-strategy).

### Security: revocation was off by one second

**Also affects projects using `auth.jwt.revocationCheck: 'kv'`.**

Revocation markers and token mint timestamps were both whole seconds, and the
check rejected only a token stamped *strictly before* the marker. A token
minted earlier in the same second as the revocation therefore compared **equal**
and survived — a sub-second hole in a mechanism whose whole promise is
"revoked within seconds." Whether an attacker landed in it was a coin flip on
clock alignment, not on anything defensible.

Markers are now epoch milliseconds, tokens carry millisecond companions
(`iatMs`, and `sctMs` for the relationship-scope proof), and the comparison
denies a tie. A token minted milliseconds before the revocation is rejected; one
legitimately re-minted *after* it in that same second is still accepted, which
is why simply flipping `<` to `<=` on seconds was not the fix.

`iat` and `sct` remain standards-compatible unix **seconds** for external
tooling — the millisecond claims are internal ordering metadata, set by the
signer from the same clock sample and never accepted from a caller.

**Rollout is conservative, not clever.** Marker format is read from an explicit
version marker, never guessed from magnitude. Where one side is from v0.60.4 or
earlier — an old token against a new marker, or any token against an old marker
— there is no sub-second information to compare, so the **entire marker second**
is revoked. That over-revocation is bounded (old tokens expire within their TTL,
old markers within the entry lifetime), and it costs the caller a `401` and a
re-authentication, exactly as any other rejected token does. No action is needed
upgrading forward.

**Do not straddle this release, though.** A worker from before v0.61.0 reads a
millisecond marker as `NaN` and concludes *not revoked* — it does not recognise
it as corrupt, so the fail-closed path added above never fires. Rolling back
across this release, or running two differently-versioned deploy targets against
one shared KV namespace, means every revocation written by the new version is
**silently unenforced** by the old one until the entry expires (~19 minutes at
default settings). Upgrade targets that share a namespace together. If you must
roll back, treat outstanding bearers as unrevoked for that window: lower
`expiresIn`, or purge the `qbrev:` keys and re-stamp from the old version.

## v0.60.4 — August 4, 2026

### Fix: retired-path cleanup was inert in v0.60.1–v0.60.3

The stale-file cleanup shipped in v0.60.1 never ran. Both ends were correct —
the compiler resolved the retired paths, the CLI was ready to delete them — but
the v2 compile stream's `done` event didn't carry the field, so it arrived
`undefined` and the CLI dutifully deleted nothing.

The event is a hand-maintained projection of the compiler's output, and nothing
type-checks that it stays complete: a field added on both sides but missed
there is dropped in transit with no error anywhere. The only symptom was an
absent side effect.

The underlying duplication is gone rather than patched: the event is now
derived from the compiler's output by omitting `files`, instead of three
separate hand-maintained lists (the server builder, the CLI's type, the CLI's
reconstruction) that all had to agree. A new field reaches the CLI for free. A
test still asserts the contract so the intent stays visible.

If you compiled on any of v0.60.1–v0.60.3, the stale root `openapi.json` /
`asyncapi.json` / `schema-registry.json` are still on disk. Recompile on
v0.60.4 and they are removed.

## v0.60.1 — August 3, 2026

### Fix: moved files no longer leave orphans behind

Upgrading to v0.60 left the old `openapi.json`, `asyncapi.json`, and
`schema-registry.json` sitting at your project root. The compiler wrote the new
copies under `src/apps/__quickback/spec/`, but nothing removed the old ones —
on a real project that was **6.7 MB of stale specs**, still committed, that
tooling pointed at the old path kept reading in place of the live ones.

That is worse than a clean break: a stale spec drifts from your API silently,
where a missing file fails immediately.

The root cause was structural, not a one-off. The compiler's output is a
manifest of files to **write**, with no way to express "and drop what you wrote
last time at a path I no longer use." The CLI's pre-write wipe covers `src/`
only, so any path that relocates outside `src/` leaves an orphan forever.

The compiler now also returns **retired paths**, and the CLI deletes them on
your next compile. See [Retired paths](/tooling/output#retired-paths).

Every filter errs toward keeping a file, because a wrong deletion is
unrecoverable while a missed one is only untidy — a moved path is removed only
once its replacement is actually emitted, a path the current compile still
writes is never removed, and nothing may escape the output directory.

No action needed: recompile and the stale files go.

## v0.60.0 — August 3, 2026

### Spec blobs move out of the Worker bundle

`openapi.json`, `asyncapi.json`, and `schema-registry.json` are no longer
imported by the Worker. They are emitted into `src/apps/__quickback/spec/` and
served through the Cloudflare [ASSETS
binding](https://developers.cloudflare.com/workers/static-assets/).

Previously the specs shipped as text imports parsed lazily on first request.
That kept their object graphs out of the isolate startup window, but the *bytes*
still sat in the script V8 has to parse on every cold start. On a large API
that is measured in megabytes, spent to serve discovery routes most callers
never request. As assets they leave the bundle entirely.

Measured on a 40-feature production API (paired runs, alternating, same
machine):

| | Before | After | |
|---|---|---|---|
| Bundle (raw) | 23,610 KiB | 17,218 KiB | **−6,392 KiB (−27%)** |
| Bundle (gzip) | 3,027 KiB | 2,762 KiB | −266 KiB (−8.8%) |
| Startup CPU (median) | 258 ms | 232 ms | −26 ms (−10%) |
| of which GC | 77 ms | 70 ms | −7 ms |

The bundle numbers are exact. The CPU figure is a local `wrangler check
startup` median whose run-to-run ranges overlap, so treat −10% as the shape of
the win rather than a precise constant — and note it scales with spec size: a
project with a small spec will see little.

Two consequences worth knowing:

- Projects now get an `[assets]` block even with **no** CMS, Account, or custom
  app configured.
- `run_worker_first = true` in that block is **security-load-bearing**, not a
  performance setting. It routes every request through your Worker first, so an
  asset is served only when the Worker fetches it — which is what keeps
  `__quickback/spec/` reachable exclusively through the gated spec routes. A
  `publish: 'admin'` spec is not a public URL. Do not relax it.

The `[[rules]] type = "Text"` block added in v0.59 is gone; nothing imports the
specs now.

Routes and payloads are unchanged: `GET /openapi.json` returns the same spec
under the same `openapi.publish` gate. See [Output
structure](/tooling/output#spec-blobs-are-assets-not-bundle).

> **BREAKING — `openapi.json` is no longer written to the project root.**
> Tooling that read it (`npx openapi-typescript openapi.json`, CI steps, lint
> rules) must point at `src/apps/__quickback/spec/openapi.json`.
>
> `api-types.gen.ts` is **unaffected** — the compiler still generates it at the
> project root, which is why most projects never invoke `openapi-typescript`
> directly and need no change at all.
>
> On your first compile after upgrading, the CLI deletes the now-orphaned root
> copies. It only does so for blobs it just re-emitted under the new path, so an
> older compiler that still writes to the root is never second-guessed.


## v0.59.1 — July 29, 2026

### Fix: auth and features D1 bindings could collapse onto one name

A project whose database config carried a single `binding` (e.g.
`binding: 'DB'`) emitted **two** `[[d1_databases]]` entries bound to the same
name, while the generated code still called `env.AUTH_DB`. Half the runtime
therefore reached a database with no auth tables.

In split mode — the default for `cloudflare-d1` — `binding` names the
**features** database only; auth is a separate D1 with its own `authBinding`.
The service resolver was letting auth fall back to the generic `binding`, so one
value drove both.

It went unnoticed because three places resolved the auth binding independently
and only one carried that extra fallback, so the reserved-name validator
computed `AUTH_DB`/`DB`, saw no clash, and passed while the wrangler emit
computed `DB`/`DB`. All three now agree.

A binding collision in split mode is also a **compile error** now rather than a
silently broken deploy, naming `authBinding` / `featuresBinding` and the
`splitDatabases: false` single-database alternative as the fixes.

Not introduced by v0.59.0 — the same behaviour is present in v0.58.3 and
earlier. If you hit it, set `authBinding` explicitly or upgrade.

## v0.59.0 — July 29, 2026

### BREAKING · Masking: sensitive columns now require an explicit decision

**The compiler no longer decides whether to mask a column. It refuses to
compile until you do.**

Quickback has always detected sensitive column names (`email`, `ssn`, `apiKey`,
`stripeSecret`, `refreshToken`, …) and silently attached a default mask rule
plus a warning. That was wrong in both directions: it broke list views nobody
asked it to break, and it created a false sense of protection. Worse, only
*some* surfaces read the auto-detected set — through v0.58.3 the HTTP response
path applied **declared-only** masking while realtime and live views applied
detected-plus-declared, so an auto-detected secret was redacted over WebSocket
and returned in **plaintext** on `GET /:id`, list, `POST` and `PATCH`, and
stayed freely filterable and sortable. Making detection non-silent removes that
whole bug class by construction: the set of masked columns is now exactly the
set you declared, so no two surfaces can disagree about it.

Every detected column must now carry an explicit decision in `masking`:

```typescript
masking: {
  apiKey: { type: 'redact', show: { roles: ['admin'] } },  // yes, mask it
  email: false,                                            // no, reviewed
}
```

`false` is a new first-class answer meaning **reviewed — not sensitive here**:
the value is returned in full and the column stays freely `?filter=`-able,
`?sort=`-able and `?search=`-able. An omitted key is not a decision.

#### Migrating

Compile the project. For each error, you get the resource name, every undecided
column, its suggested rule, and a paste-ready `masking` block with both answers
per column — delete the wrong line from each pair:

```
customers: 2 columns match Quickback's sensitive-column patterns but carry no masking decision.
  ...
  masking: {
    email: { type: 'email', show: { or: 'owner', roles: ['admin'] } },  // mask it
    // email: false,  // …or this instead: reviewed, not sensitive here
  },
```

Two ways to resolve each column:

1. **Mask it** — keep the suggested rule (or write your own). The value is
   redacted for callers outside `show.roles`, and only those roles may
   filter/sort/search it. Widen the query surface independently with
   `query: { roles: [...] }`; drop `or: 'owner'` if the table has no owner
   column.
2. **Don't mask it** — `column: false`. Nothing changes about how the column
   behaves; you've recorded that somebody looked.

A false positive (`cc` on an accounting table, `webhook` on a config row) costs
one line. That is the intended trade: the compiler asks about everything it
recognises rather than guessing.

Detection itself is unchanged — same keyword table, same suffix matching, and
the suggested rule is still `show: { or: 'owner', roles: ['admin'] }`. It is
now a suggestion in an error message rather than a default that applies on its
own. See [Masking](/define/masking).

### Security fix: live views and realtime subscribe now evaluate `read.access`

The live-view materialize route (`GET /api/v1/views/:view/:rootId`) and the
view-subscribe ws-ticket lane applied the firewall and masking but **never the
role gate**. A firewall is a tenant filter, not a role gate, so a same-org
member who was correctly `403`'d on `GET /<table>/:id` received the full row —
and every included child row — through the view, and could subscribe to its
live deltas.

Every table on a [live view](/platform/realtime/live-views#security) surface (the
root *and* each included child) is now gated by its own `read: { access }`,
**deny-on-any**: a root denial is a `403`, an included-table denial collapses to
`404` with the firewall miss. Both entry points enforce it, and the gate lives
inside the materializer so resync inherits it too.

Shapes the view path cannot decide now **fail closed at compile time** instead
of silently passing: function-form access, `relationship:`/`fga:` arms, and a
`record:` condition on an *included* table. A `record:` condition on the root
is evaluated post-fetch against the materialized row, the same two-phase gate
`GET /:id` uses.

### Security fix: a team-only firewall no longer opens every tenant

A resource whose **only** tenant predicate was team-scoped emitted no `WHERE`
filter at all when `ctx.activeTeamId` was absent, so every authenticated caller
could read, update and delete every tenant's rows.

The team predicate is rendered drop-when-absent — correct while an org arm still
bounds the query (the teamless rest-of-org slice), but with team as the sole arm
"drop the team filter" meant "drop the entire tenant boundary". The claim is
genuinely absent in ordinary setups: the Better Auth teams sub-plugin is opt-in,
and the external auth provider never supplies it. Auto-detection also derives
team-only isolation for any table carrying a `teamId` and no `organizationId`,
with no explicit `firewall` block.

An absent claim now **denies** when team is the sole tenant arm. Soft-delete and
literal predicates are row filters, not tenant boundaries, so they don't count as
a co-existing arm. Team alongside org is unchanged.

Paired with a compile-time gate: a team-only firewall on a project without the
teams sub-plugin is now a build error rather than a resource that denies every
request, since that claim can never be populated. See
[Firewall](/define/firewall).

### Security fixes: managed file storage

Three scope-expansion holes in the managed file subsystem (`managed: true`):

- **Removed organization members kept reading private files.** The files worker
  read `activeOrgId` straight off the session row; a session outlives
  membership, and a bucket with no `readRoles` had no second gate. Membership is
  now re-checked per request and a missing member row clears the organization
  before any org-scoped check.
- **JWT claims were trusted as current.** The worker's JWT fast path took `orgId`
  and `role` from the signed token — a mint-time snapshot. Both are now
  re-derived from the database. The lookup only runs when a request carries an
  org claim, so user-scoped and public reads stay lookup-free.
- **`writeScope: "user"` buckets bound nothing to the owner.** Keys come from a
  client-supplied path and the R2 write landed *before* the unique-key metadata
  insert could reject it, so a member who knew a peer's key overwrote their
  object — with no undo. Presigned uploads were worse: they signed a `PUT` URL
  and deleted the victim's metadata row. Both paths now check ownership first,
  and for presign that check runs *before signing*, since the URL is itself the
  write capability. The metadata row is also reserved before the R2 write, so a
  failed insert can no longer leave an overwrite behind.

See [File storage](/platform/storage/r2).

## v0.58.1–v0.58.3 — July 28, 2026

> No changelog entries were written for these three patch releases. Reconstructed
> from git: compilation and generated-output hardening (`fc7d54fb`) and a
> string-keyed batch FK visibility set that removes spurious `fkNotFound`
> (`96f764ed`) in `0.58.1`; a CI deploy-serialization fix in `0.58.2`
> (`2bf00e9c`). `0.58.3` was published from CI and has no bump commit in this
> repository — its contents are not reconstructible from git history here.

## v0.58.0 — July 28, 2026

### `?include=<ownsRelation>` — the aggregate reader lands

`read.include` now accepts **owns relation names** alongside FK columns:
`?include=reactions` on a changeset root embeds the aggregate's owned child
rows (pivot on the root PK, match the child's declared `fk`), landing in the
same top-level `included` map keyed by include token, then row PK. The child's
full security surface applies exactly as for FK embeds — pre-record-evaluable
`read.access` (compile-enforced), firewall ANDed into the fetch on the
caller-claims handle, masking, and `fields[<token>]` sparse projection. The
`?fields=` pivot guard follows the direction: an owns embed requires the
**source PK** in the projection (an FK embed still requires the FK column).
This closes the changeset design's phase-2 reader — write the aggregate with
the changeset media type, read it back with `?include=`.

### `afterCommit.reindex` + `afterCommit.notify` — the 06 fast-follows land

The two deliveries deferred out of afterCommit v1 now ship, riding the same
post-commit dispatch seam (best-effort, `waitUntil`, never rolling back the
committed changeset):

- **`reindex: true`** re-enqueues the committed **root row** for embedding
  regeneration on the same at-least-once `EMBEDDINGS_QUEUE` lane the plain
  routes use — after the tx commits, so a rolled-back changeset produces no
  orphan jobs. Fail-closed: requires the root's `embeddings` block; honors
  `onInsert`/`onUpdate: false`; a field-watch array means "always" on the
  aggregate lane. The owns validator's embeddings warning now clears for a
  reindex-declaring root (owned children still warn — root row only).
- **`notify`** sends a device push via the web-push provider (compile error
  without `push: { provider: 'web-push' }`). The audience is a
  `defineRealtimeDelivery` profile resolved with the **same disclosure
  gates as `broadcast`** — masked columns, `q.stamp` proven identity, and
  child-id enumeration are shared code, not a parallel set. Title/body/url/
  tag are `{key}` templates over a gate-checked payload map; an unmapped
  placeholder is a compile error, a null value at runtime pushes to nobody.
  Scope-keyed deliveries fan to `scope:<room>` subscription tags; user
  deliveries to the user's devices; roles AND-compose.

A reindex- or notify-only `afterCommit` no longer requires realtime
infrastructure — the ws-infra gate now applies only to `broadcast`/`realtime`
lanes.

## v0.57.1 — July 28, 2026

> No changelog entry was written for this release. Reconstructed from git: a
> polish pass over `0.57.0` — `q.table` column initializers that cannot be
> resolved now throw instead of dropping silently (`ace6cf17`), the scope-grant
> gate distinguishes "permission arm present" from "no arm at all"
> (`1b7a5773`), tenant-scope arrow endpoints and object-thunk references parse
> again so authz analysis is restored (`ad53b924`), and the anchored fallback
> fails closed (`48c70370`).

## v0.57.0 — July 28, 2026

### Typed scoped `db` on every Neon mode + write-side audit forbids

The action `db` is now schema-aware on **all** Neon connection modes, not just
Hyperdrive: http mode gets the same insert relaxation and typed reads with
`.transaction` **blocked at the type level** (the neon-http driver throws at
runtime — the type now says so instead of a 500), websocket mode gets the full
interactive-tx flavor. With D1 already typed, every supported provider's action
`db` now carries the schema — including the `SealedValue` brand on `.sealed()`
columns, so string-processing a sealed value is a compile error everywhere.

On every typed provider, the compiler-managed audit keys are now `?: never` in
`.insert().values()` **and** the newly-typed `.update().set()`: the audit
wrapper hard-stamps them from the verified caller, so a supplied value was
always discarded — now it's a compile error instead of a silent lie.
`.set({ deletedAt })` stays allowed (the soft-delete signal).

### Hard-stamp unification — one audit-actor contract

The scoped db's dead "fill-if-absent" actor lane is gone: on every wrapped
handle, `createdBy`/`modifiedBy` come from the verified principal, period (the
raw client remains the documented escape for system writes that own their
provenance). Three attribution gaps closed with it: cron schedules' and queue
handlers' `withInternalContext` now hand out an **audit-wrapped** db, so
`system:cron-<name>` / `system:queue-<name>` actually land on audit columns;
upserts into encrypted/sealed tables now stamp `modifiedAt`/`modifiedBy` on the
`onConflictDoUpdate` conflict branch like every other write; and an org-scoped
child table whose source can't accept the injected `organizationId` column now
fails the compile instead of silently shipping without its tenant column.

### Visible audit columns — `...q.audit()` / `...q.softDelete()` + canon validator

The compiler-managed audit quartet and soft-delete pair can now be **declared visibly**
in authored feature files: q-DSL tables spread `...q.audit()` / `...q.softDelete()` into
the columns object (types flow through — `$infer` shows `createdAt`/`modifiedBy`/… on the
author's own table object), raw Drizzle tables declare the canonical lines literally.
`quickback migrate visible-columns` codemods an existing project in one shot; emitted
output is unchanged either way (the spreads expand to the exact injector lines).

With visibility comes validation, **on by default**: every feature table must declare its
managed columns visibly (`...q.audit()` / `...q.softDelete()` or the canonical literal
lines — one `quickback migrate visible-columns` run), and every managed column that appears
in authored source is checked against the canonical spec — insert-time default chains on
`createdAt`/`modifiedAt` (+`$onUpdate`), nullable no-default actor columns, nullable
no-default `deletedAt`, canonical SQL names, never `.encrypted()`/`.sealed()`. Divergence
(silently accepted before) and missing declarations **fail the build**, with soft-delete
presence pinned to `crud.delete.mode`. Opt out with
`compiler.features.visibleAuditColumns: false`, which downgrades divergence to warnings and
restores invisible injection. Unknown spreads in a q-DSL columns object are now a compile
error instead of being silently dropped.

Also in this line: `q.table`'s `$insert` type is honest (required vs defaulted/stamped
columns; audit keys are `?: never` — the wrapper hard-stamps them), `$infer` includes the
audit columns, and the canonical column definitions collapsed into one shared spec
(`constants/managed-columns.ts`) consumed by the injectors, meta augmentation, request
validation, spread expansion, and the new validator.

## v0.56.0–v0.56.11 — July 21–27, 2026

> No changelog entries were written for the v0.56 line (twelve releases).
> Reconstructed from git: feature-area follow-through — generated CRUD/changeset
> routes claimed by an area prefix (`d33262e8`), feature-area transition route
> adapters (`05d57940`), a fail-closed guard for generated routes under an area
> prefix (`23f1f58d`), and a run of cross-feature resolution fixes
> (`b6984a22`, `747dfad4`, `fb0cfb41`, `26bc569f`, `ae76e3e8`) — plus
> relationship-proven `SESSION` parity for `q.stamp` (`b11fabfe`), an RLS
> serial-sequence sweep with an audit-schema latch and `fga_tuples` backstop
> (`299c93ee`), changeset trigger parity and `define-action` typing fixes
> (`4c64edab`), and R2 files-migration scheduling gated on managed files
> (`29b908f2`).

## v0.55.1 — July 21, 2026

### Feature areas — hierarchical authz grouping (`_area.ts`)

A folder under `features/` carrying an `_area.ts` (`export default defineArea({...})`) is now
an **area**: it declares a route prefix + admission lanes (the same shape as a project
namespace, incl. `mintScope`/`acceptScope`) and an authz vocabulary — relationships, composed
roles, named gates, tenant anchors, scope kinds — that every feature inside the folder
references by bare name. The vocabulary is visible to the area's subtree ONLY: the compiler
checks every reference channel (access roles/gates/scoped aliases, action `access`/`anchor`,
firewall `via`/`{ rule }`/`{ tenant }` arms, `ctx.scope.<kind>` accessors, auth-view tenant
includes, realtime handshake roles, table-level namespace lanes, and other `_area.ts` nodes)
and fails the build on any outside reference. Root `authz` stays the global level, and root
bodies may reference root-declared names only (no laundering an area name project-wide).

Fail-closed rules shipped with it: re-declaring an inherited name is a compile error (no
overrides in v1); a descendant action whose path opts out of the area prefix must carry an
explicit `route: 'self'` marker; relative `path: './…'` resolves against the area prefix with
dot segments rejected; foreign features can't claim paths (or table-level namespace prefixes)
under an area's prefix; nested areas may add vocabulary but not mount a second prefix under a
mounted ancestor (unlinked parent/child admission never compiles); and area-tree projects
must declare `requires: ['feature-areas']` — the marker reaches the compiler through ANY CLI
version, so an area-unaware toolchain is rejected instead of silently dropping nested
features. Area projects also emit a reviewed audit artifact, `quickback/authz-manifest.json`
(area tree, declared names, member features, area-gated vs self-gated actions, root-config
namespace consumers), and the generated `AGENTS.md` gains a feature-areas section.

Byte-parity guarantee: a flat root-authz project re-expressed as an area tree compiles to
byte-identical output (the emit-site key-order determinism pass from v2 step 0 makes the fold
order provably irrelevant). See [Feature areas](/define/areas).

One cosmetic side effect of that determinism pass: the first recompile of an existing project
on this release may reorder generated relationship-table **import lines** (they are now
emitted in sorted order instead of authz-map insertion order). The reordering is a one-time,
behavior-free diff.

### Access rules: named gates (`authz.rules`) and tenant anchors (`authz.tenants`)

Two new root-`authz` blocks let you declare a row/role rule once and stamp it by name instead
of copying predicates across files. A **named gate** in `authz.rules` bundles up to three
arms — `who` (roles), `record:` (record-aware predicates), and `firewall` lanes — under one
name, referenced by bare name in any `access: { roles: [...] }` array (applies who + record)
and as `{ rule: '<name>' }` in a resource's `firewall:` (splices the lanes into the top-level
AND array — a reference can only *narrow* row admission, never widen it). A **tenant anchor**
in `authz.tenants` names the tenant predicate (`{ column, equals }`, claim side locked to
verified ctx claims) and is written `{ tenant: 'org' }` in firewalls — with an optional
per-site column override — and `tenant: 'org'` in auth-view includes. Expansion happens at
compile time; emitted SQL is byte-identical to the inline spelling.

Everything fails closed: gate/role/scoped-role/`+`/`:` name collisions, `+` hierarchy sugar
inside rule bodies, `{ rule }` inside `all`/`any` groups, firewall-less gates referenced from
`firewall:`, firewall-bearing gates on actions or in `masking.show`, and an access-side
reference to a firewall-bearing gate whose lanes the resource doesn't also stamp are all
compile errors. Gates in `realtime.requiredRoles` lower to static who-roles at the handshake;
a declared non-empty list that lowers to nothing is a compile error, never a silent `[]`. A
new `AUTHZ_UNKNOWN_ROLE_NAME` warning flags reference-shaped names that resolve to nothing.
See [named gates](/define/authz-roles#named-gates-authzrules) ·
[`{ tenant }`](/define/firewall#named-tenant-anchors-tenant) ·
[`{ rule }`](/define/firewall#named-gate-references-rule).

## v0.55.0 — July 17, 2026

### Generated projects ship an `AGENTS.md`

Every compiled project now emits an `AGENTS.md` at its root — the file coding agents read first — weaving Quickback's security model and a *"when to use what"* decision guide into the output. An agent editing the features is steered to the right primitive: a **view** for a filtered read, a **transition** for a guarded state change, an **action** for custom logic, **guards**/**masking** for field-level control, **named invalidations** for realtime refresh — each with a `docs.quickback.dev` link. It also documents the access tiers (incl. `SESSION`/`SCOPED`), the `WHERE column = value` firewall model with the org-OR-scope `{ any: [...] }` pattern for non-org callers, and the fail-closed coherence guardrail (with the *"never `exception: true` as a workaround"* rule). Regenerated on every compile.

