Skip to main content

5.2 Release Notes

Patch Releases

All patch release notes for 5.2.x are available on the releases page.

Querying

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 (see 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.

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.

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.

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 for the full checklist, including per-record cost and how to memoize per-request work on context.

Configuration

Replicated set_configuration

The set_configuration operation now accepts "replicated": true to apply a configuration change to all cluster nodes in a single Operations API call, with per-node outcomes reported in the response's replicated array. Only cluster-appropriate parameters should be replicated — see Configuration Operations.