diff --git a/reference/database/schema.md b/reference/database/schema.md
index 7ebaa1ea..5855077e 100644
--- a/reference/database/schema.md
+++ b/reference/database/schema.md
@@ -558,6 +558,119 @@ How the one definition applies at each scope (when permission checking is active
Constraints: the check must be synchronous, side-effect free, and fast — it can run once per candidate record visited during traversal (verdicts are memoized per query). `this` is the frozen record during per-record evaluation. A thrown exception denies that record (fail closed). On caching tables the check is enforced against the record actually returned, after any source revalidation.
+### Record-Level Access Control for Writes (Record-Scoped `allowDelete` / `allowUpdate` / `allowCreate`)
+
+
+
+The write-side authorization hooks get the same record-scoped treatment when overridden. Unlike `allowRead`, they stay on the resource (records do not expose them): during per-record evaluation `this` is a per-row resource instance with the record loaded, so schema attributes read naturally (`this.ownerId`) and `super.allow*(...)` composes the role/RBAC baseline. Because the write paths are asynchronous, `async` overrides participate too (they are awaited, fail-closed).
+
+```javascript
+export class Reports extends tables.Reports {
+ allowDelete(user, target, context) {
+ // Compose the table/RBAC grant first, so losing the role's delete denies.
+ if (!super.allowDelete(user, target, context)) return false;
+ if (user.role.permission.super_user) return true;
+ return this.ownerId === user.id; // per matching record
+ }
+ allowUpdate(user, updates, context) {
+ // `this` = the EXISTING record's resource; `updates` = the incoming data.
+ return this.ownerId === user.id;
+ }
+ allowCreate(user, record, context) {
+ // No existing record on a create — the incoming record is the parameter.
+ return record.ownerId === user.id;
+ }
+}
+```
+
+How the overridden hooks apply (when permission checking is active, e.g. any external request):
+
+- **Conditional (query-shaped) `DELETE`** — an overridden `allowDelete` is evaluated once per matching record, with **filter semantics**: rows it denies (including by throwing or rejecting — fail closed) are skipped and the permitted rows are deleted, returning success rather than a blanket 403. `limit`/`offset` count _allowed_ rows, so denied rows don't consume pagination slots.
+- **Array `PUT` into a collection** — with an overridden `allowUpdate`/`allowCreate`, each element is authorized individually: an element whose record already exists is checked with `allowUpdate` (`this` = the existing record's resource), a new element with `allowCreate`. Any denial fails the **whole request** with a 403 and aborts the transaction — no partial batch is committed (each element was an explicit write target, unlike a query's incidental matches).
+- **Single-record writes** (`PUT`/`PATCH`/`DELETE` on an id) — unchanged: evaluated at request entry with the record loaded, so the same override works there too.
+
+The default (non-overridden) hooks are table-level RBAC checks and keep their single request-entry evaluation with no per-record cost — including the collection-scope insert permission governing array `PUT`s. Operations API and SQL writes are governed by role permissions only; the `allow*` hooks apply to the Resource APIs (REST and JavaScript).
+
+One caveat: a class that overrides `delete()` itself replaces the framework's record-scoped delete path — its query-shaped deletes keep the request-entry check (a warning is logged when this combination is detected), and the custom `delete()` is responsible for any row-level enforcement.
+
+For an unauthenticated caller, `user` itself is `undefined` — not an object with a falsy `id`. Leading with `super.allow*(user, ...)`, as the `allowDelete` example does, denies before any `user.*` access runs, since the default checks are written with `user?.`. An override that skips that call and dereferences `user.id` directly, as the `allowUpdate`/`allowCreate` examples do, will throw for an unauthenticated caller instead — which still fails closed (denies), just via an exception rather than an explicit `false`.
+
+### Upgrading Existing `allow*` Overrides
+
+
+
+Record scoping changes how an existing override behaves, so an application moving to 5.2 should review any `allow*` hook it defines. The change is deliberately narrow and most applications need no change at all — but a few of the cases below alter behavior _silently_ rather than raising an error, so they are worth an explicit look.
+
+#### Does this affect my application?
+
+Only if a class that **extends a table** (`tables.MyTable` or `databases.myDb.MyTable`) overrides an `allow*` hook. Two common situations are unaffected:
+
+- **An `allow*` override on a plain `Resource` subclass.** A custom endpoint that reads or writes tables internally (`tables.MyTable.get(...)`, `.put(...)`, `.search(...)`) rather than extending one is not affected — record scoping applies to a table's own query and write paths, which a plain `Resource` never enters. This is the most common shape by far, so most applications with `allow*` overrides are untouched.
+- **A table that does not override the hook.** Defaults keep their single request-entry RBAC check, unchanged and with no per-record cost.
+
+If you do override a hook on a table, two questions decide whether anything changes: does the override consult the record (via `this`), and does anything call that table with a collection read, an array `PUT`, or a conditional `DELETE`?
+
+#### A denial can become a filtered result instead of an error
+
+This is the change most likely to surprise, because the request still succeeds. Where an overridden hook previously produced one verdict for the whole request, a denial returned `403`. Under record scoping the verdict is per record, so denied rows are simply _omitted_:
+
+- A collection read whose overridden `allowRead` denies every record now returns `200` with an empty (or shorter) array instead of `403`.
+- A conditional `DELETE` whose overridden `allowDelete` denies every match now returns success having deleted nothing, instead of `403`.
+
+The verdicts themselves are unchanged — no one gains access — but callers, tests, or monitoring that assert on a `403` will see a success response instead. The pattern most affected delegates the general case to the base class:
+
+```javascript
+export class KVData extends tables.KVData {
+ allowRead(user, target, context) {
+ if (user?.role?.role === 'auditor') return true;
+ return super.allowRead(user, target, context); // ← RBAC denials now filter, not 403
+ }
+}
+```
+
+Here the override exists to special-case one role, but its presence makes the hook record-scoped for _every_ request, so an ordinary user without table read permission receives an empty `200` rather than a `403`. If you depend on the error, check the permission before the query (or at a `Resource` endpoint in front of it) rather than relying on the hook's response shape.
+
+#### For `allowRead`, `async` decides the enforcement model
+
+Per-record read evaluation happens during query traversal, which cannot await. So an `allowRead` declared `async` **cannot** be record-scoped: it keeps the single request-entry check at collection scope (and logs a warning once per class). A synchronous override with identical logic _is_ record-scoped.
+
+Adding or removing `async` on an `allowRead` therefore silently switches it between "one verdict for the request, `403` on denial" and "one verdict per record, denied rows filtered". Make the override synchronous if you want row-level filtering; keep it `async` only if it must make a whole-request decision. This applies to reads alone — the write paths are asynchronous, so `async` `allowUpdate`/`allowCreate`/`allowDelete` overrides participate in per-record evaluation normally.
+
+#### Array `PUT` consults hooks that previously never ran
+
+Before 5.2, an array `PUT` into a collection was authorized by a single collection-scope check against the insert permission. In 5.2, if the table overrides `allowUpdate` **or** `allowCreate`, every element is checked individually — existing records through `allowUpdate`, new records through `allowCreate`.
+
+Two consequences: an override that was never consulted on this path may now run for the first time, and overriding only one hook of the pair leaves the other at its RBAC default. A table that overrides `allowUpdate` alone will authorize new elements with the _default_ `allowCreate`. Review both hooks together, and note that any single denial fails the whole request with a `403` and commits no part of the batch.
+
+#### A record-independent override still runs per record
+
+An override that consults only `user` and `context` and never reads `this` returns the same verdict for every record. It stays correct, but it gains nothing from record scoping while still being called once per row — and it is subject to the filtered-result change above. To get actual row-level filtering, the override must consult the record through `this`.
+
+#### Per-record cost, and caching per-request work
+
+An overridden hook runs once per candidate record, so expensive work inside it (a lookup, a fetch) now repeats per row. The `context` object is the **same instance** across every per-record evaluation of a request, which makes it the right place to memoize per-request work:
+
+```javascript
+const ROLE_CACHE = Symbol('roleCache');
+
+export class Reports extends tables.Reports {
+ allowRead(user, target, context) {
+ let role = context[ROLE_CACHE];
+ if (role === undefined) role = context[ROLE_CACHE] = lookUpRole(user); // once per request
+ return role.canSee(this.ownerId);
+ }
+}
+```
+
+#### Checklist
+
+1. List the classes that extend a table _and_ override an `allow*` hook. Anything extending a plain `Resource` can be skipped.
+2. For each, decide whether the override is meant to be a **row-level** decision (consults `this`) or a **whole-request** one. Make `allowRead` synchronous for the former; leave it `async` for the latter.
+3. Check whether any caller does a collection read, an array `PUT`, or a conditional `DELETE` against that table — including the automatic REST routes an `@export`ed table exposes, not just your own code paths.
+4. Update anything asserting on a `403` from a collection read or conditional `DELETE`, which may now be a filtered `200`. Array `PUT` denials still return `403` — the change there is that the hook may now run for the first time (item 3).
+5. If you override `allowUpdate` or `allowCreate`, review the pair together.
+6. Move expensive per-request work in a hook onto `context`, as above.
+
### Tuning Filtered Traversal
Filtered traversal is bounded by a visit budget of `ef * filterExpansion` nodes (`filterExpansion` defaults to 24). If the budget is exhausted before the result list fills — which happens when the filter matches only a tiny fraction of records — the search returns the matches found so far rather than erroring. Both knobs can be set per query:
diff --git a/release-notes/v5-lincoln/5.2.md b/release-notes/v5-lincoln/5.2.md
index 8cea990a..c248dfcc 100644
--- a/release-notes/v5-lincoln/5.2.md
+++ b/release-notes/v5-lincoln/5.2.md
@@ -12,7 +12,33 @@ All patch release notes for 5.2.x are available on the [releases page](https://g
### Filtered Vector Search (Predicate-Aware HNSW Traversal)
-Vector searches combined with filters now evaluate the filter during HNSW graph traversal, so the query keeps exploring until it has enough matching nearest neighbors instead of post-filtering a fixed candidate set (which under-filled results under selective filters). Filters can come from query conditions, a JS-API `vectorFilter` function, or a record-scoped `allowRead` override — overriding `allowRead` on a table now makes it a row-level access check, evaluated per record with `this` bound to the record (closing the gap where a collection scan could return rows a single-record GET would deny). With it, a restricted user's vector search returns the k nearest records they are allowed to see. Very selective conditions automatically use an exact scan instead of graph traversal, and a `filterExpansion` visit budget bounds traversal cost. See [Vector Indexing](/reference/v5/database/schema#vector-indexing).
+Vector searches combined with filters now evaluate the filter during HNSW graph traversal, so the query keeps exploring until it has enough matching nearest neighbors instead of post-filtering a fixed candidate set (which under-filled results under selective filters). Filters can come from query conditions, a JS-API `vectorFilter` function, or a record-scoped `allowRead` override (see [Record-Scoped Authorization](#record-scoped-authorization) below) — with it, a restricted user's vector search returns the k nearest records _they are allowed to see_ rather than "nearest k, minus redacted". Very selective conditions automatically use an exact scan instead of graph traversal, and a `filterExpansion` visit budget bounds traversal cost. See [Vector Indexing](/reference/v5/database/schema#vector-indexing).
+
+## Record-Scoped Authorization
+
+The `allow*` authorization hooks on tables are now **record-scoped when overridden**: instead of a single collection-scope verdict per request, an application-overridden hook is evaluated once per record, with access to that record's fields. Framework defaults are unchanged — a table that does not override a hook keeps its single request-entry RBAC check with no per-record cost — and the change applies only to classes that extend a table; a plain `Resource` subclass defines its own semantics and keeps the single entry check.
+
+### Row-Level Read Access Control (`allowRead`)
+
+Overriding `allowRead` on a table now makes it a row-level access check. During collection queries — including scans, searches, and vector traversals — it is evaluated once per candidate record with `this` bound to the (frozen) record, so checks like `return this.ownerId === user.id` read naturally; rows it denies are filtered out of the results. This closes the gap where a collection scan could return rows a single-record `GET` would deny. Single-record `get(id)` keeps its request-entry evaluation with the record loaded (a denied record returns a `403`), and subscriptions grant the connection at subscribe time, then filter delivery per event so a subscriber receives only the row changes the check permits — with live subscriptions periodically re-authorized so a revoked grant tears the subscription down.
+
+Per-record evaluation requires a synchronous override (query traversal cannot await): an `async allowRead` keeps the single request-entry check and logs a warning. Verdicts are memoized per query, and a thrown exception denies the record (fail closed). See [Record-Level Access Control](/reference/v5/database/schema#record-level-access-control-record-scoped-allowread).
+
+### Record-Scoped Write Authorization (`allowDelete` / `allowUpdate` / `allowCreate`)
+
+The write-side authorization hooks get the same treatment when overridden. A conditional `DELETE` evaluates an overridden `allowDelete` once per matching record with filter semantics — permitted rows are deleted, denied rows are skipped — and an array `PUT` authorizes each element individually (`allowUpdate` for existing records, `allowCreate` for new ones), failing the whole request atomically on any denial. During per-record evaluation `this` is a per-row resource with the record loaded, so row-level checks like `this.ownerId === user.id` and `super.allow*` composition work naturally; unlike `allowRead`, the write paths are asynchronous, so `async` overrides participate in per-record evaluation too. See [Record-Level Access Control for Writes](/reference/v5/database/schema#record-level-access-control-for-writes-record-scoped-allowdelete--allowupdate--allowcreate).
+
+### Upgrading Applications That Override `allow*`
+
+Record scoping (both the read and write hooks above) changes the behavior of `allow*` overrides that already exist, so applications upgrading to 5.2 should review any hook they define. Most are unaffected — the change reaches only classes that **extend a table** and override a hook, so the very common pattern of an `allow*` override on a plain `Resource` subclass that reads and writes tables internally is untouched, as is any table left on the default RBAC hooks.
+
+Three behavior changes are worth checking explicitly, because they do not raise an error:
+
+- **A denial can become a filtered result rather than a `403`.** A per-record verdict omits denied rows instead of rejecting the request, so a collection read or conditional `DELETE` that previously returned `403` may now return a successful response with fewer (or zero) rows. No one gains access, but callers and tests asserting on the error see a success. This most often shows up in overrides that special-case one role and delegate the rest to `super.allowRead(...)`.
+- **For `allowRead`, `async` decides the model.** Per-record read evaluation happens during query traversal, which cannot await, so an `async allowRead` keeps its single request-entry check (and logs a warning) while a synchronous one becomes record-scoped. Adding or removing `async` silently switches enforcement between the two. Write hooks are unaffected by this — their paths are asynchronous, so `async` overrides are record-scoped normally.
+- **Array `PUT` now consults hooks that previously never ran.** These writes used to be authorized by a single collection-scope insert check; now each element is checked individually, so an overridden `allowUpdate`/`allowCreate` may run on this path for the first time — and overriding only one hook of the pair leaves the other at its RBAC default.
+
+See [Upgrading Existing `allow*` Overrides](/reference/v5/database/schema#upgrading-existing-allow-overrides) for the full checklist, including per-record cost and how to memoize per-request work on `context`.
## Configuration