Quickback Docs

v0.40 – v0.48

Quickback release notes for v0.40.0 through v0.48.1 (June – July 2026).

v0.48.1 — July 10, 2026

Both entries below were authored under the working label v0.49 (commit cd5bcd8f). No 0.49.x was ever published — the CLI went 0.48.10.50.0 — so 0.48.1 is the release that carries them.

Realtime identity tags — one send reaches a person on either auth door

WebSocket connections now carry identity tags, proven at ticket mint and signed into the ws-ticket (never client-supplied): user:<userId> for every Better Auth caller, plus scope:<kind>:<subjectId> whenever a scope subject is proven — on either door. A sessionless scope principal's claim and a Better Auth caller's matched relationship lane stamp the identical subject-keyed tag (both derive from the same relationship row), so senders no longer double-send per recipient to cover subject: 'both' roles.

  • targetTags on every send (realtime.broadcast, realtime.insert, …): delivery is the deduplicated union of the runtime's indexed getWebSockets(tag) lookup — no per-frame attachment scan. Replaces the userId identity arm when present; the scope/org bind and targetRoles audience still apply.
  • Fail-closed: targetTags: [] delivers to nobody, consistent with the empty-audience rule.
  • Tags are immutable for a socket's lifetime (hibernation-tag semantics); limits (10/socket, 256 chars) are validated at mint — rejected, never truncated.

Realtime authorization lease — hibernated sockets no longer outlive revocation

A ws-ticket was verified once at upgrade, and DO hibernation kept the socket authorized forever — bans, member removal, and subject revocation never touched a live connection. Connections now carry a delivery-time authorization lease (default 15 minutes, from ticket iat):

  • The fan-out skips expired connections (they receive nothing) and closes them with code 4401; inbound messages — even pings — are enforced the same way.
  • In-place renewal: { type: "reauth", ticket: "<fresh ws-ticket>" } extends the lease and refreshes roles without socket churn. Renewal is strict — same userId, same scope/org, exact same tag set (tags are immutable); identity drift closes with 4401 → reconnect fresh.
  • Renewal tickets come from POST /broadcast/v1/ws-ticket, which re-runs the full authorization gate (session, KV revocation, relationship/scope-claim probes) — so revocation propagates within one lease period, with no KV reads in the Durable Object.
  • Configure via realtime: { lease: '15m' | seconds | false } (min 60s; false disables — discouraged). Clients that never reauth degrade gracefully to one forced reconnect per lease.

v0.48.0 — July 8, 2026

Realtime broadcasts are fail-closed — explicit per-resource audience (BREAKING)

Realtime broadcasting is now opt-in per resource with a mandatory audience, mirroring how CRUD is explicit. This closes a fanout footgun where a resource that never mentioned realtime could still broadcast its rows to an entire room.

  • realtime.access: { roles: [...] } is the audience — same { roles } shape as read.access / crud.*.access. A subscriber receives a live row iff that role bucket would admit them at REST. Roles may be concrete membership/scope roles or UPPERCASE pseudo-roles (PUBLIC, AUTHENTICATED, USER, SYSADMIN).
  • Fail-closed, at compile time — a broadcasting resource with no/empty audience is a compile error naming the resource. There is no implicit fanout. To broadcast to everyone in the room, say so deliberately with access: { roles: ['PUBLIC'] }.
  • Fail-closed, at runtime — the Broadcaster now treats an empty audience as nobody (previously: everybody) and evaluates the uppercase pseudo-roles exactly as the REST access evaluator does (userRole is threaded through the ws-ticket for USER/SYSADMIN).
  • No project-wide auto-broadcastproviders.database.config.realtime now only wires the transport (Broadcaster DO + ws-ticket route); it no longer enables broadcasts on any resource.
  • Migration — a project that relied on the project-wide auto-broadcast must add an explicit realtime: { enabled: true, access: { roles: [...] } } block to each resource that should broadcast. requiredRoles is the deprecated spelling of access.roles (still compiles, normalized internally). q-events guests demonstrates the staff-lane pattern (access: { roles: ['admin','member'] }, event-scoped, masked).

Deterministic codegen output

The compiler now sorts every filesystem-discovered collection (features, and each services.* group) by a stable key before generating, so recompiles produce byte-identical output regardless of the CLI's directory-scan order. Eliminates the churny, behavior-neutral diffs (queue-consumer case order, auth blocks, schema imports) that appeared across machines/CLI versions.

v0.47.14 — July 7, 2026

acceptScope: one namespaced handler for the user AND the sessionless principal

A project-level namespace can now accept an already-minted scope claim as proof of entry — the inverse of mintScope. Set authz.namespaces.<ns>.acceptScope: '<kind>' and the hoisted resolver gains a fast-path: a scope-only principal (ctx.scopePrincipal, no ctx.userId) carrying a verified ctx.scope.<kind> claim proves entry by that claim, without a DB relationship probe. One /event/:eventId/* handler then serves both the Better-Auth user and the sessionless invite-principal — retiring the parallel PUBLIC /attendee/* twin.

  • Named-string only — the kind is always explicit; a bare true is a type error. Default absent → the resolver never admits a scope principal (fail-closed).
  • Two locks, compile-enforcedacceptScope opts the namespace in, and at least one relationship lane must confer a <kind> role declared subject: 'supplied' \| 'both'. A subject: 'user' role emits no accept arm; a kind whose every conferring lane is user-only is a compile error, not a dead fast-path. The kind must be declared and its requestField must equal the prefix's first :param.
  • Forging-safe — the claim is verified (never caller-asserted), the kind is read by name (not an any-key loop), the instance is pinned twice (entry.id === :param in the gate + eq(loads.pk, :param) in hydration), and each accept arm is one exact (role, lane) pair. New proof source, zero new enforcement surface — firewall, masking, access, and scoped-db read the same ctx.scope they read for a userId caller.
  • Composes with mintScope — a userId caller still gets a rolling re-mint; a scope principal's verified claim is preserved (not re-stamped) so its subjectId / sub-keys survive.
  • Realtime shares the same acceptance — the ws-ticket route now proves a scope principal through the same emitter as the REST gate (emitScopeAcceptance), so the two surfaces can't drift. Bringing the ws-ticket path onto it tightened it to the named-kind read + supplied-reachable role filter, closing an any-kind cross-kind match. q-events ships a vendor scope role (subject: 'both') + logistics-only view demonstrating the sessionless supplier end-to-end.
  • Writes work too — the scoped-db audit wrapper no longer throws when the caller is a verified scope principal (ctx.scopePrincipal, no ctx.userId). Instead of failing on the missing user, it stamps the synthetic, audit-only principal id (scope:<kind>:<subjectId>, the token's sub) into created_by / modified_by. So the SAME /event/:eventId/* handler body serves both a Better-Auth user and the sessionless principal on reads AND writes — no parallel raw-db write path. Still fail-closed: a genuine no-user write (no scope principal) throws exactly as before. q-events adds a vendorCheckin write under the acceptScope namespace exercising it end-to-end.

See Scopes → acceptScope.

v0.47.13 — July 6, 2026

Sessionless scope mint: ctx.mintScope + subject: 'supplied' \| 'both'

A scope can now be minted for a principal who has no Better Auth account — an event attendee arriving by email + invite code, a kiosk/handout code, a partner token. The action proves the relationship its own way and asks Quickback to mint a disciplined scope token from the row it proved.

  • authz.scopes.<kind>.roles.<role>.subject: 'user' \| 'supplied' \| 'both' (default 'user'). 'both' makes the same scope:event:attendee reachable by a logged-in user (via /scope/v1/enter) and a sessionless invite-holder (via ctx.mintScope) — one firewall arm, one masking rule, both principals.
  • ctx.mintScope({ via, subject }) — hydrated on ctx in any action (including PUBLIC) when a supplied/both role is declared. The action supplies only the conferring row's primary key; Quickback reads the role, resource id, sub-keys, and subject id off that row — never caller-asserted, so the role can't be forged. Returns a scope-only JWT (no user identity): ctx.userId stays undefined, so user/org/owner gates and the AUTHENTICATED/USER pseudo-roles all fail closed; only scope:<kind>:<role> gates admit it.
  • ctx.scope.<kind>.subjectId — a reserved claim carrying the proven subject row's PK, populated on both proof paths. Scope an attendee's self-data (travel, dietary, todos) with a single firewall arm ({ field: 'guestId', equals: 'ctx.scope.event.subjectId' }) that serves both a user-attendee and a sessionless one.
  • Realtime — the ws-ticket authorizes a scope-only principal directly off its proven ctx.scope claim, so accountless attendees join the same rooms with the same scope role. The where: filter runs on the mint probe, so a cancelled row confers nothing (revocation within one ≤180s mint cycle).

See Scopes → Sessionless scope mint.

MCP OAuth: split authorization-server / resource-server (multi-app SSO)

You can now run one canonical authorization server with many resource servers (each hosting its own /mcp) over a shared auth DB:

  • A resource server that sets agents.mcp.requireAuth now emits the full OAuth access-token verification path on its own — previously the JWKS-verify block only emitted when the separate auth.config.oauth.enabled flag was set, so a requireAuth-only resource server fell back to HMAC + session and 401'd every valid externally-issued bearer.
  • Point a resource server at an external AS with auth.config.oauth.issuerUrl; the middleware verifies incoming bearers against the shared JWKS (iss/aud/exp + live-session binding).
  • auth.config.oauth.audiences (an array) registers federated resource-server audiences on the AS (RFC 8707). It now unions with the AS's own origin instead of replacing it, so listing federated servers never drops the AS's own /mcp audience. Also exposed on the plugin-form oauthProvider.validAudiences.

See Agents → Split authorization server.

MCP tools: callable path params + typed input schemas

An action mounted under a path that carries a parameter — a namespace prefix like /scope/:scopeId/..., or any path: with :params — now produces a fully callable MCP tool:

  • The mounted route's path params are extracted into the tool's inputSchema (and _pathParams), so the model has a field to supply each one. Previously only record-based actions emitted a param (the hard-coded id); a standalone or namespace path param was absent from the schema, the URL resolved with an empty segment, and the action was uncallable over MCP.
  • Standalone routes are stored in Hono colon form (:scopeId); the OpenAPI spec key (and therefore the tool's _path) is now normalized to brace form ({scopeId}) so the runtime's brace-only path interpolation can fill it.

Action input schemas also lower to richer JSON Schema instead of a phantom empty {}: z.coerce.*, z.literal, unions of literals, and nested z.object (child keys stay nested) now emit their real types, so the model — and the OpenAPI spec — see the actual field shapes. See Agents → MCP tools.

Scaffold surfaces MCP/OAuth + secrets opt-ins

quickback create now seeds the generated quickback.config.ts with curated, commented-out hints for the authenticated MCP server (agents.mcp.requireAuth), the split-AS oauth.issuerUrl/audiences, and bindings.secrets — so these opt-ins are discoverable in the config itself, not only in the docs.

Custom secrets via bindings.secrets

Declare a custom secret your feature/action code reads off env — name and contract only, never the value:

bindings: {
  secrets: [
    { name: "WEBHOOK_SIGNING_SECRET", description: "HMAC for webhook callbacks", required: true },
  ],
}

It's emitted into the generated env.d.ts Env interface so env.<NAME> type-checks, and required secrets join the boot-time env guard — the worker fails closed with 503 MISSING_ENV if one is unset. The value is set out-of-band (wrangler secret put); the schema rejects any inline value so a real secret can never be committed. See Bindings → secrets.

Auth: resolve-email-context.ts hook for per-recipient OTP context

Drop quickback/hooks/resolve-email-context.ts and the emailOTP send calls it per recipient, spreading the returned object into your email templates — branded palettes (organizer vs. attendee), plan tier, locale, etc. — without patching generated output. Fail-open, with the Drizzle handle passed in. See Auth Hooks → Email context resolver.

Codegen fixes

  • Schedules: handler import paths are now rebased on relocation, so a cron handler importing ../../features/... resolves correctly from the generated src/lib/schedules.ts instead of failing the worker build.
  • OAuth/MCP middleware: the auth-schema tables are imported under aliased names so a function-scoped const session can no longer shadow the session table import — closes a temporal-dead-zone crash that silently 401'd every OAuth/MCP bearer.
  • MCP route: the large tool-data literal is split into a mcp-tools.ts data module (suppressed) so it can't overrun tsc's type-instantiation budget; the mcp.ts route logic stays fully type-checked.

Realtime: identity-gated Durable Object rooms (roomIdEquals)

Per-user Durable Object entrypoints (gateway / presence / inbox) can now declare an identity gate on the binding — access: { roomIdEquals: 'ctx.userId' } (or 'ctx.activeOrgId'). The compiler emits a Worker-edge gate at /realtime/<kebab>/* that 401s anonymous callers, 400s a missing segment, and 403s unless the decoded room segment equals the server-resolved ctx id — a string compare with no backing table or DB query. See PartyServer rooms → Per-room authorization.

  • Closes the cross-user hole that "no access declared" leaves open: a per-user room without a gate falls through to the authenticated-only catchall, so any authenticated user could open another user's room by guessing the id.
  • The gate lives at the edge, not in the DO — a Durable Object has no session and only sees client-settable headers, so it must never re-authorize from request data (trusting an x-user-id header was the original spoof hole).
  • Only 'ctx.userId' and 'ctx.activeOrgId' are accepted; anything else is a compile-time error (fail-closed Zod + generator validation).

OAuth provider: spurious authorization-server warning silenced by default

The generated oauthProvider({...}) now emits silenceWarnings: { oauthAuthServerConfig: true } by default. The compiler always serves the /.well-known/oauth-authorization-server discovery route when the provider is enabled, so @better-auth/oauth-provider's "please ensure … exists" boot warning was always spurious. Override or extend it via plugins.oauthProvider.silenceWarnings.

defineSchedule — cron jobs from a single source

Declare a cron schedule in services/schedules/*.ts and the compiler emits the Worker's scheduled() handler + the matching [triggers] crons in wrangler.toml — the trigger-side mirror of defineQueue. No separate cron Worker, no hand-patching the generated entry (which a recompile would strip). See Schedules.

// services/schedules/sweepDigests.ts
export default defineSchedule({
  name: "sweepDigests",
  cron: "* * * * *",
  execute: async ({ db, env, services }) => { await runDigestSweep(db, env, services); },
});
  • One scheduled(event, env, ctx) dispatcher runs every schedule whose cron matches event.cron; failures are isolated per-schedule.
  • The execute context ({ db, env, services, cron, scheduledTime }) runs server-internal with ctx.internal = true and an audit actor of system:cron-<name>, plus a withInternalContext helper for roles: ['INTERNAL']-gated rows.

Rate limiting — the fifth security pillar

Per-operation request caps on every CRUD endpoint, backed by Cloudflare's native Rate Limiting binding — no extra storage, no third-party libraries. Applied to every resource by default (read 1000/60s, write 200/60s), overridable per-op, per-resource, or project-wide; false opts out at any scope.

  • One middleware per feature router maps method→op, keys the limit by <resource>:<op>:<userId> (falling back to the client IP for anonymous callers), and returns 429 + Retry-After when over the cap.
  • Unique (limit, period) tuples collapse onto shared bindings — a project that never declares rateLimit ships exactly two (RL_1000_60, RL_200_60), emitted as GA [[ratelimits]] blocks in wrangler.toml and typed RateLimit on CloudflareBindings.
  • period is compile-time constrained to 10 or 60 (Cloudflare binding limit); the check no-ops when a binding is absent, so local dev never hard-depends on it.

v0.47.8 — June 12, 2026

Postgres-style table triggers (defineTable / feature triggers:)

Tables can now declare before/after triggers on insert/update/ delete, with two lowering lanes and a per-trigger compile report. See Triggers for the full reference.

  • sql: entries compile to real SQLite CREATE TRIGGER statements, shipped in the journaled features migration (content-addressed — an unchanged trigger set appends nothing; removing a trigger drops it on the next migrate). They fire for every write path including raw SQL, run atomically with the triggering statement (the only transactional option on D1), support when: guards and RAISE(ABORT, ...) rejection, and OLD./NEW. column references are validated at compile time. The compiler owns the BEGIN/END frame — emitted uppercase, since lowercase begin breaks remote D1's statement splitter. On soft-delete tables, *Delete events map to the deleted_at transition (plus a hard-delete variant) and *Update events exclude it, so a soft delete fires delete triggers exactly once. D1 only.
  • handler: entries run as application hooks at the audit-wrapper write chokepoint (REST CRUD + actions). before* hooks are synchronous and can mutate the payload or throw to reject — the write never executes and the request gets TRIGGER_REJECTED (422); audit fields are re-stamped after hooks and cannot be forged. after* hooks run once per returned row and are awaited — a throw surfaces as TRIGGER_AFTER_FAILED (500) with details.committed: true since the write already persisted. Handlers get { table, op, values|row, ctx, db, env }; the db is audit-wrapped at cascade depth + 1, so hook writes stamp audit fields, emit live-view deltas, and fire nested triggers, bounded by a depth cap of 5. Each table's drizzle object is auto-imported into the generated registry under its authored name, and handler imports are usage-filtered and hoisted.

The compile output includes a lowering/coverage report and fail-closed warnings: handler: triggers do not fire for queue handlers, cron, or raw SQL — guards that must hold universally should use sql: + RAISE(ABORT). async: true (queue-dispatched after-hooks) and withOld: true (previous-row access) are reserved and rejected at compile time until they ship.

Also in this change:

  • New trigger error layer with TRIGGER_REJECTED / TRIGGER_AFTER_FAILED codes in the generated error catalog, OpenAPI spec, and SDK types.
  • Parser: function-valued properties in defineTable/feature configs now parse as compile-time source carriers instead of failing the whole config with "no dynamic expressions" — unblocks trigger handlers (and function-form access: / custom masks) through the CLI source path.
  • Fixed a column-injection bug where a comment merely mentioning a column name (e.g. "deleted_at" in a docblock) suppressed audit/soft-delete column injection for the table.
  • Fixed a silent no-op: a project declaring services/queues/*.ts handlers without embeddings or additionalQueues deployed a wired queue export with no [[queues.consumers]] to feed it — the handlers were dead code. The primary queue (and its typed env binding) is now emitted for handlers-only projects, with a compile notice that Cloudflare Queues requires the paid Workers plan. Queue emission remains strictly opt-in by usage: projects with no queue-backed features (embeddings, queue handlers, additionalQueues, webhooks) emit no queue configuration and deploy on the free plan.

v0.47.7 — June 12, 2026

Envelope encryption — q.text().encrypted() (encryption pillar, Phase 1)

Mark a text column .encrypted() and it is AES-256-GCM-encrypted at rest under a per-organization key: a database breach (backup, export, leaked token, SQL injection, insider read) yields only ciphertext for that column. See Encryption.

  • Per-org DEKs live wrapped under the ENCRYPTION_KEK worker secret in a compiler-owned org_data_keys table (lazily created, versioned for rotation). Deleting an org's row crypto-shreds every value it wrote.
  • One write chokepoint: CRUD, batch, and action-layer db.insert/update all encrypt through the audit wrapper — audit snapshots, webhook payloads, and realtime deltas carry ciphertext by construction.
  • Reads decrypt behind the firewall then apply masking; encrypted columns are compile-time excluded from ?search/?filter/?sort, view query allowlists, and embedding sources.
  • ctx.crypto.encrypt/decrypt for explicit server-side processing in actions; boot guard refuses to serve without the KEK (503 MISSING_ENV); scaffolded .dev.vars now includes a generated ENCRYPTION_KEK.
  • The sealed (E2EE) and vault tiers remain compile-gated until built — roadmap in apps/compiler/research/encryption-vault-spec.md.

v0.47.0 — June 11, 2026

Breaking: zero-false-positive analyzer checks are now compile errors

The authz/authn analyzer (v0.37) shipped warn-only. After a release cycle of data, the checks whose every trip is a definite misconfiguration — never a legitimate config — are promoted to compile errors. Warnings nobody is forced to read are documentation; the pillar guarantees are now enforced.

Breaking — a config that previously compiled with one of these warnings now fails the compile:

  • AUTHZ_EMPTY_PERMISSION — a permission that is structurally unsatisfiable (allOf(a, not(a)): a fact AND its exact negation at the same AND-level).
  • AUTHZ_CAPABILITY_CEILING — a resource's firewall grants a scope:<kind>:<role> role access beyond its capability profile (or the role has no profile at all). This is the M0.2 pillar guarantee — it was authored as an error all along and is now enforced.
  • AUTHN_ROUTE_COVERAGE, AUTHN_SURFACE_INVARIANT, AUTHN_SECRET_CONSISTENCY, AUTHN_CREDENTIAL_DELIVERY, AUTHN_CLAIM_PROVENANCE — the structural-authn set (route auth coverage, bearer-only /mcp/agent surfaces + INTERNAL unreachability, mint↔verify key/issuer/audience pairing, anchored credential delivery, verifier-only identity claims). These still run against empty route/credential metadata on a real compile (no-ops until the generators emit it), but the severity contract is locked in now — they fail the build the moment they can fire.

The heuristic/advisory checks are unchanged: AUTHZ_SHADOWED_ARM, AUTHZ_UNREACHABLE_ROLE, AUTHZ_UNREACHABLE_VIEW, and AUTHZ_NOT_OVER_RESOURCE stay warnings; AUTHZ_PUSHDOWN_COST stays an info report. QUICKBACK_AUTHZ_STRICT=true still promotes those warnings too.

The override escape hatch — every promoted finding can be explicitly accepted, per finding, with a required reason:

authz: {
  analyzer: {
    allow: [
      {
        check: 'AUTHZ_CAPABILITY_CEILING',
        target: 'sessions/scope:event:organizer',   // from the rendered finding
        reason: 'organizer grant profile lands with the v2 rollout (TICKET-123)',
      },
    ],
  },
}

An override downgrades that exact (check, target) finding back to a warning that cites the reason — even under strict mode (it's an explicit, reasoned acceptance). Every rendered error prints its exact override recipe. Guard rails: check must be one of the promoted ids (unknown or keep-warn ids are rejected at config validation), reason is required, and an allow entry that matches no current finding is itself a compile error — no dead overrides accumulating after the underlying issue is fixed.

Analyzer-internal defects are still fail-open: a bug in the analyzer itself downgrades to a single warning and the compile continues. Only real findings fail builds.

CLI: live views now compile through the CLI

defineView files under quickback/services/views/ were never loaded by the CLI — the compile payload simply omitted them, so live views (v0.35) only worked when driving the compiler API directly. The CLI now discovers and ships them like every other service definition, and the compile summary counts them (Loaded services: … 1 live view(s)). If you declared views and wondered why no surface ever went live: upgrade the CLI and recompile.

CLI: deterministic compile output across machines

Feature and service discovery used raw readdir order, which differs between macOS and Linux — route registration order, schema re-exports, OpenAPI paths, and the Neon RLS document's content hash could all churn when the same project was recompiled on a different machine. All discovery sites now sort by codepoint, so the same input compiles to the same bytes everywhere. Expect a one-time diff on the first recompile if your previous output was generated in a different filesystem order.

Fixes

  • Read-only resources (no crud block) generated routes importing a CRUD_ACCESS symbol their resource file never exported — a tsc/bundle break. The export is now unconditional.
  • The blog starter template gated writes on the removed ADMIN pseudo-role, which hard-fails current compiles; it now uses the org-membership admin role.
  • reports/operations.manifest.json and reports/conformance.manifest.json now also cover the realtime (/broadcast/v1), storage (/storage/v1), and FGA (/fga/v1) surfaces.
  • The dead free/pro tier plumbing is gone: no Tier row in quickback whoami, no upsell copy at login. Compiling complete projects requires login, exactly as before — there was never a paid gate behind it.
  • The CLI is published to npm via trusted publishing (GitHub OIDC) — signed provenance, no long-lived npm token in the pipeline.

v0.46.0 — June 11, 2026

Breaking: "no organization context" responses unified on HTTP 403

Generated APIs used to answer the same semantic condition — the caller has no active organization context — with two different statuses: 400 ORG_REQUIRED from the collection list guard and the create/PUT/batch org validation, but 403 ACCESS_NO_ORG from standalone actions and 403 ACCESS_TENANT_SCOPE_REQUIRED from unsafe org-scoped actions. Clients and SDKs had to special-case the split.

Breaking — recompile + redeploy required; every "no org context" denial is now 403:

  • GET /api/v1/<resource> (and views) without an active org or ?organizationId=403 ORG_REQUIRED (was 400).
  • POST /, PUT /:id, POST /batch, PUT /batch without an active org → 403 ORG_REQUIRED (was 400).
  • Standalone actions (403 ACCESS_NO_ORG) and unsafe org-scoped actions (403 ACCESS_TENANT_SCOPE_REQUIRED) were already 403 and are unchanged.

Error codes are unchangedORG_REQUIRED, ACCESS_NO_ORG, and ACCESS_TENANT_SCOPE_REQUIRED keep their identities, so clients that discriminate on code need no changes. Only clients that branch on the raw 400 status for these responses must update to 403.

Breaking: root-mounted actions lose their nested alias URLs

A standalone action whose declared path: lives outside its feature prefix (e.g. path: "/reports/orders" in the orders feature) mounts at the root: /api/v1/reports/orders. That root URL is the one the OpenAPI spec, the MCP surface, and the operations manifest have always advertised — but the generated per-feature routes file also carried an undocumented, fully-gated copy at the nested /api/v1/<feature><path> URL (e.g. /api/v1/orders/reports/orders).

Breaking — the nested alias is no longer emitted. One URL per operation: a root-mounted action exists only at its advertised /api/v1<path> mount. Anything calling the unadvertised nested URL gets a 404 after recompile + redeploy; switch to the documented root URL. Actions declared under the feature prefix are untouched, and a feature whose standalone actions are all root-mounted no longer emits an (empty) feature actions routes file.

Behavior change: PUBLIC inside or: arms admits anonymous callers

Access has always documented PUBLIC as working on every access node, but the emitted auth gate only honored it in a flat roles: [...] list. A tree like

access: {
  or: [
    { roles: ["PUBLIC"] },
    { roles: ["admin"] },
  ],
}

still answered anonymous callers with 401. The auth gate is now tree-aware and matches the documented semantics: a PUBLIC or: arm admits anonymous callers past the authentication gate (an and: group admits them only when every arm does), and the access / org / firewall layers still evaluate the full tree. One shared predicate now drives the emitted gates, the OpenAPI security blocks, and view routes (which gate per view on the view-effective access).

Worth a loud note: if you have a nested PUBLIC arm that was silently 401-ing, that endpoint becomes anonymously reachable after recompile — exactly what the config declares, but verify it's what you meant. PUBLIC audit logging applies to the newly-admitted routes as usual.

Realtime broadcasts now enforce requiredRoles and masking

A table's declared realtime gates weren't being applied to the generated CRUD broadcasts: realtime: { requiredRoles: [...] } and column masking were honored on the HTTP surface but not on the WebSocket fanout. The docs promised per-role, masked realtime delivery; the generated broadcasts now match it. Recompile and redeploy any project using realtime on role-gated or masked tables.

Generated realtime.insert / update / delete calls now carry:

  • targetRoles — the table's realtime.requiredRoles (post role- hierarchy + expansion), enforced per subscriber at fanout.
  • maskingConfig — serialized from the same effective masking (declared + auto-detected sensitive columns) the HTTP mask functions are generated from. What HTTP masks is what the WebSocket wire masks.

Live-view view_changes deltas honor the same table-level requiredRoles at fanout, so a role-gated table can't leak through a live view subscription either.

Notes:

  • Recompile suffices — no config change needed.
  • If you worked around this with hand-written realtime.* calls passing targetRoles / maskingConfig, those workaround broadcasts can be removed.
  • type: 'custom' mask functions can't ride the wire — the Broadcaster fails closed and broadcasts [REDACTED] for those columns.
  • Tables without requiredRoles or masking are byte-identical: the open broadcast default is unchanged.

Batch operations inherit the base operation's access

When a batch operation was declared without its own access, it didn't pick up the base operation's role requirement:

crud: {
  update: { access: { roles: ["admin"] } },
  batchUpdate: { maxBatchSize: 25 },   // ← no access of its own
}

A batch config declared without access now inherits the base operation's access at compile time — fail closed, so runtime, the OpenAPI spec, and the generated code all agree. Explicit batch access still wins (including per-side on upserts); a base op without access still yields an authenticated-only batch op. The flat update: { batch: true } shape and auto-promoted batch ops were never affected. Recompile and redeploy if you use the explicit-batch shape without per-batch access.

Record actions, PUT upserts, and legacy firewall arms fail closed

Three fail-closed hardening fixes, found and verified against the new conformance catalog (route × principal expectations derived from the compiler's own IR — see the conformance manifest). All apply on recompile + redeploy; no config change needed:

  • Record actions evaluate the role gate before fetching the record, so the denial response no longer varies with record state — a uniform 403 ACCESS_ROLE_REQUIRED. (Bulk actions already had this order; unsafe actions write their audit row on the pre-record denial.)
  • PUT upsert treats an out-of-scope id as a firewall miss (403 reveal / 404 hide, per your firewall error mode; a standard 207 miss entry on PUT /batch) instead of routing it into the create path. Genuinely new ids keep the create path.
  • Legacy (array-form) firewall arms deny cleanly when a bound context claim is absent at runtime (e.g. no active org), rather than surfacing a database error — the same deny-when-absent behavior the explicit all: / any: group forms already had.

Root-mounted standalone actions use the standard gate stack

Standalone actions mounted outside their feature prefix (path: "/reports/orders") were emitted through a separate code path that did not apply the full access / audit / scoped-database treatment that in-feature standalone actions receive. They now render through the same generator, so the same access checks, security-audit writes, and tenant-scoped database wrapping apply everywhere. If you use root-mounted standalone actions, recompile and redeploy. (Unifying the two code paths is also what surfaced the two breaking changes above.)

Namespace routing fixes

  • An action whose declared path exactly equals its namespace prefix is now claimed by the namespace like any nested action (Hono's /x/* mount already matched the bare /x) — it's served through the namespace's gate instead of carrying a second inline copy of the resolver.
  • A namespace with zero claimed standalone actions (e.g. record-based-only) no longer mounts its middleware at all — a dead gate can no longer intercept requests under its prefix.

Custom org roles work with Better Auth member management

Custom roles referenced in access trees (e.g. roles: ["support"]) are now auto-registered with the Better Auth organization plugin. They were already enforceable on routes but unprovisionable through Better Auth — invite-member / update-member-role rejected them with 400 ROLE_NOT_FOUND, so there was no supported way to actually give a member the role. Auto-registered roles carry member-equivalent permissions (this is about assignability; your access trees remain the authorization model). If you pass your own roles / ac to the organization plugin options, those win and auto-registration is skipped. Built-in, pseudo, and scope: roles are excluded; projects with no custom roles emit byte-identical output.

snake_case tables: dropped record actions and broken helpers fixed

Two fixes for tables declared with snake_case names (event_notes):

  • The generated .quickback/define-action.ts helpers imported the camelCase identifier while the generated alias file exports the verbatim declared name (export { event_notes }) — a TS2724 compile failure for any snake_case table with action files. The helpers now import the real export aliased to the camelCase local.
  • Record actions on a snake-declared table living in a kebab-named feature file (event-notes/actions/pin.ts) were not emitted — missing from both the generated routes and OpenAPI. They now emit and document consistently.

If you have snake_case tables with record actions, recompiling adds routes that previously didn't exist — review the new surface.

First-class MCP OAuth config: auth.oauth

The OAuth 2.1 surface behind agents.mcp.requireAuth (v0.42.0) gains a first-class config block — env-resolvable issuer / login / consent URLs, scopes, RFC 8707 audiences, and a dynamic-client-registration toggle:

auth: defineAuth("better-auth", {
  oauth: {
    enabled: true,
    issuerUrl: { env: "OAUTH_ISSUER_URL", fallback: "http://localhost:8787/auth/v1" },
    loginPage: { env: "OAUTH_LOGIN_URL", fallback: "http://localhost:8787/account/login" },
    consentPage: { env: "OAUTH_CONSENT_URL", fallback: "http://localhost:8787/account/consent" },
    scopes: ["openid", "profile", "email", "offline_access"],
    audiences: [{ env: "OAUTH_MCP_AUDIENCE", fallback: "http://localhost:8787/mcp" }],
    allowDynamicClientRegistration: true,
  },
}),

Setting it auto-enables the oauthProvider, jwt, and bearer plugins, emits the RFC 8414 / RFC 9728 discovery documents, and accepts the provider's JWKS-signed access tokens in the standard auth middleware. A new project hook, quickback/hooks/resolve-oauth-context.ts, lets you derive tenant fields (activeOrgId / activeTeamId / roles) from the validated OAuth identity. On Neon, the auth schema is now owned by the Better Auth CLI end to end (the compiler no longer emits its placeholder src/auth/schema.ts or src/lib/neon.ts). See OAuth Provider (MCP-ready) and Auth Hooks.

Neon RLS covers force-enabled plugin tables

On Neon, row-level-security policies are generated for the Better Auth tables. The list of tables receiving policies was derived from your declared auth config and didn't account for plugins the compiler force-enables automatically — so some auth-owned tables could be created without RLS policies. The list now derives from the fully resolved plugin set, so every generated auth table receives appropriate policies (self-scoped where there is a clear owner, service-role-only otherwise — fail closed). The RLS migration is content-addressed, so the update arrives as a new journaled entry on the next recompile + db:migrate. Neon projects should recompile and run db:migrate.

New compile reports: operations + conformance manifests

Every compile now writes two machine-readable reports next to openapi.json:

  • reports/operations.manifest.json — a stable, sorted index of every operation: { operationId, method, path, auth, feature, kind }, covering the resource, action, view, auth, realtime, storage, and FGA surfaces. An extend-only public contract — the recommended integration point for scripting against the generated surface.
  • reports/conformance.manifest.json — for each route × principal class, the expected outcome (allow / 401 / 403 / cross-tenant miss …), derived from the same IR the emitters consume. This is the artifact the conformance fixes above were found and verified against.

The OpenAPI generator itself now reads the same IR as the route emitters, so the spec follows the emitted surface by construction — including four accuracy fixes: PUT /{id} is documented only when actually enabled, the batch surface documents the emitter's auto-promotion + access inheritance, and views document their effective (view-level, falling back to read-level) access. See Operations manifest.

v0.45.0 — June 9, 2026

auth.jwt.revocationCheck: 'kv' — sub-TTL JWT revocation

The JWT fast-path trusts signed claims for their full lifetime — that's what makes it a fast-path. Until now the only revocation bounds were the TTL (plain claims) and the carry-forward cap (scope claims, v0.44.0). The long-promised 'kv' mode is now real on the Cloudflare runtime:

auth: { jwt: { revocationCheck: 'kv' } }
  • Per-principal revocation timestamps, not per-token denylists. A revocation event writes "everything for this principal minted before now is dead" to the project's existing KV binding (keys prefixed qbrev:) — nothing extra to provision. Entries self-expire after the maximum token lifetime, so the store never accumulates.
  • The bearer fast-path checks before trusting. A verified token whose iat (or sct, for the relationship-scope claim) predates the stored timestamp is rejected and falls through to session auth, which re-checks membership and re-mints — an active legitimate session recovers transparently. Costs 2+ edge-cached KV reads per bearer verify; the explicit trade the opt-in buys.
  • Written automatically where revocation actually happens: org-member removal and role change (Better Auth organizationHooks), and UPDATE/DELETE of scope-conferring relationship rows — through the same audit-wrapper write chokepoint Live Views uses, plus the single DELETE route (which has the row in hand but no .returning()). Revoke-on-touch: a false positive just forces a cheap re-prove.
  • User bans flip automatically. The Better Auth admin plugin's ban lands as a user-row update, so a generated databaseHooks.user.update.after hook stamps the user-level revocation the moment banned flips — every outstanding JWT dies on its next use, and the cookie/session path already rejects banned sessions, completing the lockout. When outbound webhooks are configured, the same hook emits a user.banned event so subscribed endpoints are notified.
  • App-code helpers for everything else: src/lib/jwt-revocation.ts exports revokeUserTokens / revokeMemberTokens / revokeScopeTokens (e.g. call revokeUserTokens from a custom revoke-all flow).
  • Fails open. A KV outage degrades revocation to the TTL / carry-forward-cap bound — it never locks every bearer out.

Bun/Node + 'kv' fails at compile time with a clear message (the store rides the Cloudflare KV binding; those runtimes own immediate-revoke via the cookie/session path). Default remains 'none'; without the flag the emitted middleware is byte-identical to v0.44.0.

Neon: npm run db:migrate now applies the RLS layer

The RLS SQL (helper functions, policies, triggers) used to be emitted as orphaned drizzle/migrations/0099–0102 files that no tool ever applied — db:migrate ran the schema migrations and silently skipped the security layer. The RLS document now rides the journaled drizzle set in quickback/drizzle/ as a content-addressed migration (<idx>_quickback_rls_<hash>), so one npm run db:migrate applies schema and security, in dependency order. The migration is idempotent (DROP POLICY IF EXISTS + re-create), so it converges over databases where the old files were applied by hand; an unchanged security config appends nothing (the journal stays byte-stable across recompiles), and a changed one appends a new content-addressed entry — append-only, like every other migration. Existing Neon projects: recompile + db:migrate, nothing breaks.

Neon: user_sessions is real, and the SQL layer speaks TEXT ids

get_active_org_id() — the helper every org-scoped RLS policy reads — was querying a public.user_sessions table that no migration created, and the helper layer was typed uuid against Better Auth's TEXT ids (org_abc123), which would throw on first contact. Now:

  • user_sessions is compiler-owned migration state (src/db/rls-support.schema.ts), created by the normal journaled flow with a FK to the users table — outside src/auth/schema.ts, which the Better Auth CLI rewrites.
  • Generated Better Auth session hooks mirror the session's active organization (and team, in teams mode) into it on create/update. Switching to "no org" nulls it through — SQL-side access revokes, fail closed.
  • The RLS helper layer (get_active_org_id, get_active_team_id, is_owner, the audit helpers) is TEXT-typed to match Better Auth ids.
  • Policies for Better Auth's own tables use the plural physical names the BA CLI actually generates on Neon, and BA tables no longer get Quickback audit triggers for columns they don't have.

Hard deletes wire into the audit pipeline correctly

Two codegen bugs around delete: { mode: 'hard' }:

  • Firewall auto-detection emitted an isNull(deletedAt) predicate for tables with no soft-delete column — every read on a hard-delete table referenced a column that doesn't exist.
  • Hard-delete routes import the logHardDelete audit helper, but the helper file was only emitted when the project also had unsafe or PUBLIC actions — a project with hard deletes and neither failed at deploy time. Hard deletes are now a first-class trigger of the security-audit pipeline (helper file, AUDIT_DB binding, env types, audit migration); on D1 that means auditDatabaseId is required, and the compile error says so.

Generated output is now typechecked — with the first crop of fixes

Every compiler change now compiles a matrix of fixture projects and runs tsc + wrangler deploy --dry-run against the generated output — the class of bug where the compiler emits syntactically valid TypeScript that fails downstream. The first runs caught real ones, all fixed here:

  • Neon: src/db/org.schema.ts re-exported tables that don't exist after the Better Auth CLI's plural rewrite; a missing AuthDatabase type export; transaction callbacks typed bare (tx) failing noImplicitAny; the auth middleware's member-table fallback hardcoded the singular name.
  • Embeddings: the queue consumer's Ai.run result and the optional EMBEDDINGS_QUEUE binding both failed strict checks — the route now returns a structured 503 when the producer binding is missing instead of crashing.
  • snake_case relationship tables: relationship/arrow emitters imported the camelCase identifier where the generated file exports the verbatim table name (export { event_guests }) — TS2724 for any snake_case relationship table. Imports now alias the real export (event_guests as eventGuests).

v0.44.0 — June 9, 2026

Scope carry-forward is now bounded (revocation fix)

The auth middleware's rolling refresh re-signs a verified scope claim on every authed call without re-probing the relationship — that's what lets a scoped session ride past the short JWT TTL with zero per-request DB cost. But unbounded, it defeated the revocation model the docs promised: a removed guest/staff who kept polling got a fresh TTL on every request, so "removal takes effect within one TTL" only held for idle clients, and the refresh also bypassed the ≤180s scope-token ceiling (it re-signed with the raw auth.jwt.expiresIn).

Scope tokens now carry a proof timestamp (sct), stamped exclusively by the proven mint sites (POST /scope/v1/enter and the namespace resolver, via the shared mint helper). The rolling refresh:

  • refuses once the claim's proof is older than the carry-forward cap (15 minutes) — past it, the token expires within one TTL and the client re-proves via enter or any minting namespace route, which won't re-mint a revoked role. Worst-case post-revocation window for an active client: cap + one TTL from the last real proof, instead of unbounded.
  • carries sct forward verbatim — the refresh can never restart the window without an actual relationship probe. Claims without sct (pre-0.44 tokens) fail closed and simply stop refreshing.
  • signs through the same ≤180s ceiling as the mint sites — an auth.jwt.expiresIn above the ceiling no longer leaks into scope-bearing tokens via the refresh path.

No client changes needed: tokens renew exactly as before inside the window, and re-entering is the existing flow. For tables where even the capped window is too wide, gate with a via arm (live relationship subquery per request) instead of a bare ctx.scope.<kind> arm; immediate revocation (auth.jwt.revocationCheck: 'kv') remains on the roadmap.

v0.43.0 — June 4, 2026

Better Auth 1.6

Generated projects are now pinned to Better Auth 1.6. The agents.mcp.requireAuth work in 0.42.0 surfaced a long-standing version drift: the compiler emitted better-auth: ^1.5.0, but @better-auth/oauth-provider resolves a tree that requires @better-auth/core ^1.6.14 and imports @better-auth/core/utils/host + /redirect-uri — subpaths that only exist in 1.6.x. The result was a wrangler deploy that failed at bundle time with Could not resolve … and never uploaded.

This release aligns the entire Better Auth dependency set to a coherent 1.6.9 floor so npm resolves one consistent tree:

  • better-auth, @better-auth/drizzle-adapter, @better-auth/api-key, @better-auth/passkey, @better-auth/oauth-provider^1.6.9 (generated projects, the baked Docker deps, and the bundled CMS/Account SPAs).
  • Added an explicit kysely ^0.28.16 pin: Better Auth 1.6's migrator imports DEFAULT_MIGRATION_TABLE / DEFAULT_MIGRATION_LOCK_TABLE from kysely but doesn't declare kysely, so the Worker bundle needs it pinned directly.

No code migration was required — Quickback already uses @better-auth/oauth-provider (not the now-deprecated OIDC Provider plugin), and emits no freshAge/SAML usage affected by 1.6's behavioral changes. Verified end to end: a requireAuth project and a normal project both npm install a clean 1.6.14 tree and bundle with wrangler deploy --dry-run with zero resolution errors.

Larger migration histories no longer hit the upload ceiling

Long-lived projects accumulate one full-schema drizzle snapshot per migration, and the whole cumulative meta payload (snapshots + journals + .sql, across both the auth and features dirs) is re-sent on every compile — even a no-schema-change one. Projects with deep migration histories were hitting the compiler's existingFiles size limit and failing to compile, with no schema drift and nothing to fix on their end.

The server-side ceilings are raised so this is no longer the binding constraint:

  • existingFiles payload: 4 MiB → 16 MiB
  • total request body: 8 MiB → 32 MiB (kept above the per-category limits so it doesn't trip first)

This is enforced entirely on the compiler, so no CLI upgrade is needed — the new headroom applies as soon as compiler.quickback.dev is on this release.

Custom response headers (security.headers)

The generated Worker emitted a fixed set of security headers (CSP, HSTS, X-Content-Type-Options, …) with no supported way to add your own — teams that wanted, say, an X-Server-Version build stamp on every response had to post-patch the generated middleware after each compile.

New security.headers sets arbitrary response headers natively. Values are static strings or templates interpolating per-deploy Worker env vars via the {{env.VAR_NAME}} token:

security: {
  headers: {
    // Both vars must be set, or the header is omitted entirely.
    'X-Server-Version': '{{env.SERVER_VERSION}}+{{env.SERVER_BUILD}}',
    'X-Powered-By': 'Quickback',
  },
}
  • Omit-if-unset — a header that references env vars is emitted only when every referenced var is set (non-empty) at runtime. A Worker that forgot to inject the var emits no header, never X-Server-Version: undefined. Static headers are always emitted.
  • The value is read from c.env per request, so it picks up whatever Worker var is injected at deploy time — nothing is baked at compile time.
  • Managed headers are rejected — names owned by a dedicated security field (CSP, HSTS, X-Frame-Options, Referrer-Policy, …) error at compile so the two emitters can't race. Malformed {{env…}} tokens error too, rather than leaking literal braces into the wire header.

Survives a clean quickback compile with no post-processing. See Security Headers → Custom response headers.

v0.42.0 — June 4, 2026

Interactive OAuth for the generated MCP server

MCP connectors (Claude, Cursor, …) could only connect to a generated /mcp endpoint anonymously — they never launched a sign-in flow, so auth-gated tools stayed invisible. The OAuth machinery was already present (Better Auth's oauth-provider ships /oauth2/authorize, /token, /register + PKCE under /auth/v1); the compiler just never let a connector discover or trigger it.

New agents.mcp.requireAuth makes the MCP surface authenticated-only:

agents: { mcp: { requireAuth: true } },

When enabled, the compiler:

  • Challenges unauthenticated /mcp with 401 + WWW-Authenticate: Bearer resource_metadata="…" (RFC 9728 §5.1) — the signal connectors use to start OAuth.
  • Force-enables oauthProvider + bearer, so /oauth2/authorize, /token, and /register (RFC 7591 dynamic client registration) exist with PKCE.
  • Delegates /.well-known/oauth-authorization-server to Better Auth's real metadata instead of a hand-rolled stub that advertised a non-existent authorization endpoint.
  • Adds the <origin>/mcp resource to validAudiences (RFC 8707) so the connector's access token isn't rejected.

Login and consent are served by your Account SPA, so users authenticate with whatever methods you already enabled. Default is unchanged (anonymous public tools); set requireAuth: true for servers meant for authenticated users.

v0.40.2 — June 3, 2026

Passwordless plugins can no longer bypass the signup gate

Better Auth's emailOtp and magicLink plugins auto-create an account when a code/link is verified for an unknown email — and that path is independent of emailAndPassword.disableSignUp. As a result, account.auth.signup: false closed POST /auth/v1/sign-up/email but left OTP / magic-link verification as an open back-door for account creation.

The compiler now emits each plugin's own disableSignUp flag, and new per-plugin options expose it directly:

auth: defineAuth("better-auth", {
  emailAndPassword: { enabled: true },
  plugins: {
    emailOtp: { disableSignUp: true },   // OTP sign-in only; unknown emails rejected
    magicLink: { disableSignUp: true },  // same for magic links
  },
}),

Both default to following the global gate: setting account.auth.signup: false (→ emailAndPassword.disableSignUp: true) now also gates the OTP and magic-link paths automatically. Set { disableSignUp: false } on a plugin explicitly to keep its sign-up open while password sign-up stays closed.

v0.40.1 — June 2, 2026

  • The generated wrangler.toml and Env type now emit the ASSETS binding type for projects that mount SPAs (CMS / Account), so handler code that references c.env.ASSETS typechecks.
  • Compiler Docker builds preserve their layer cache across rebuilds, cutting local recompile time.

v0.40.0 — June 2, 2026

R2 file storage is now presign-only by default. Adding defineFileStorage("cloudflare-r2") emits just the ctx.storage signer — no FILES_DB database, no /storage/v1/* endpoints, no files worker. You sign presigned URLs against a bucket and store references in your own table. The turnkey managed subsystem is still available, now behind managed: true.

This decouples the lightweight primitive from the heavyweight subsystem so you can add uploads without standing up a metadata database you don't use.

Presign-only by default; managed is opt-in

// Presign-only (default) — ctx.storage against a bucket, nothing else
defineFileStorage("cloudflare-r2", { bucketName: "my-app-media" })

// Managed subsystem — FILES_DB + /storage/v1/* + files worker
defineFileStorage("cloudflare-r2", { managed: true })
  • New projects can omit bucketName — it defaults to <project>-media, so setup is one wrangler r2 bucket create plus the R2 API-token secrets.
  • Existing projects set bucketName to a bucket they already have. Presign-only emits no R2 bucket binding and no FILES_DB binding — nothing to provision, nothing to migrate.

Per-call bucket + custom secret names

// Sign against a different bucket than the configured default
await storage.signPutUrl(key, { bucket: "my-app-public", contentType });

If your project already stores R2 credentials under different env-var names, map them so the signer reads yours:

defineFileStorage("cloudflare-r2", {
  bucketName: "my-app-media",
  presign: { accountIdEnv: "CF_ACCOUNT_ID", accessKeyIdEnv: "S3_KEY", secretAccessKeyEnv: "S3_SECRET" },
})

Fixes

  • maxFileSize now accepts a human string ("100mb", "5gb") as well as a number of bytes — previously a string emitted invalid TypeScript.
  • Presign-only no longer emits a stray FILES_DB [[d1_databases]] block (with an invalid database_id = "local-files") into wrangler.toml, which would have made wrangler deploy reject the worker.

On this page

v0.48.1 — July 10, 2026Realtime identity tags — one send reaches a person on either auth doorRealtime authorization lease — hibernated sockets no longer outlive revocationv0.48.0 — July 8, 2026Realtime broadcasts are fail-closed — explicit per-resource audience (BREAKING)Deterministic codegen outputv0.47.14 — July 7, 2026acceptScope: one namespaced handler for the user AND the sessionless principalv0.47.13 — July 6, 2026Sessionless scope mint: ctx.mintScope + subject: 'supplied' \| 'both'MCP OAuth: split authorization-server / resource-server (multi-app SSO)MCP tools: callable path params + typed input schemasScaffold surfaces MCP/OAuth + secrets opt-insCustom secrets via bindings.secretsAuth: resolve-email-context.ts hook for per-recipient OTP contextCodegen fixesRealtime: identity-gated Durable Object rooms (roomIdEquals)OAuth provider: spurious authorization-server warning silenced by defaultdefineSchedule — cron jobs from a single sourceRate limiting — the fifth security pillarv0.47.8 — June 12, 2026Postgres-style table triggers (defineTable / feature triggers:)v0.47.7 — June 12, 2026Envelope encryption — q.text().encrypted() (encryption pillar, Phase 1)v0.47.0 — June 11, 2026Breaking: zero-false-positive analyzer checks are now compile errorsCLI: live views now compile through the CLICLI: deterministic compile output across machinesFixesv0.46.0 — June 11, 2026Breaking: "no organization context" responses unified on HTTP 403Breaking: root-mounted actions lose their nested alias URLsBehavior change: PUBLIC inside or: arms admits anonymous callersRealtime broadcasts now enforce requiredRoles and maskingBatch operations inherit the base operation's accessRecord actions, PUT upserts, and legacy firewall arms fail closedRoot-mounted standalone actions use the standard gate stackNamespace routing fixesCustom org roles work with Better Auth member managementsnake_case tables: dropped record actions and broken helpers fixedFirst-class MCP OAuth config: auth.oauthNeon RLS covers force-enabled plugin tablesNew compile reports: operations + conformance manifestsv0.45.0 — June 9, 2026auth.jwt.revocationCheck: 'kv' — sub-TTL JWT revocationNeon: npm run db:migrate now applies the RLS layerNeon: user_sessions is real, and the SQL layer speaks TEXT idsHard deletes wire into the audit pipeline correctlyGenerated output is now typechecked — with the first crop of fixesv0.44.0 — June 9, 2026Scope carry-forward is now bounded (revocation fix)v0.43.0 — June 4, 2026Better Auth 1.6Larger migration histories no longer hit the upload ceilingCustom response headers (security.headers)v0.42.0 — June 4, 2026Interactive OAuth for the generated MCP serverv0.40.2 — June 3, 2026Passwordless plugins can no longer bypass the signup gatev0.40.1 — June 2, 2026v0.40.0 — June 2, 2026Presign-only by default; managed is opt-inPer-call bucket + custom secret namesFixes