Skip to Content

v0.1.91 — 12 Aug 2026

Performance — App tier runs as a multi-process cluster

Who is affected: Every deployment. Review WEB_CONCURRENCY before upgrading.

The standalone build used to start one Node process, so a single event loop served every request — roughly 80 req/s on the deployed instance with ~30% of a 4-vCPU host permanently idle. The Docker image now starts cluster.js, which forks one worker per available core and shares the listening socket.

Measured locally (400 VUs against /api/health, 1 vs 4 workers):

Metric1 worker4 workers
Throughput2,608 req/s4,830 req/s
Average latency153 ms83 ms
p95164 ms112 ms
Worst case30.65 s4.19 s

Each worker is a full Next.js server (~400 MB). The default is one worker per core, so a container sized for a single process can OOM-loop after this upgrade — set WEB_CONCURRENCY explicitly when the container memory limit is tighter than cores × 400 MB.

Cross-worker coordination is handled by a new message bus over cluster IPC: cache invalidation, runtime-extension reloads, and cron reloads published by any worker reach all of them. The cron scheduler runs on a single leader worker (with automatic failover), so a job scheduled every minute fires once per minute rather than once per worker. Outside cluster mode everything dispatches in-process, so single-process behavior is unchanged.

Known limitation: the in-memory log ring buffer and its SSE stream remain per worker, so the Logs page shows the serving worker’s output rather than the whole cluster’s.


Performance — Permission resolution: one cached fetch instead of six queries

Who is affected: Every authenticated API request gets faster; no action needed.

Every authenticated request used to re-derive the caller’s permissions through a chain of RPCs and selects — measured at 9 Supabase round trips per non-admin request (5 for admins) on GET /api/items/daas_users, with a p50 of 360 ms against 97 ms for the data query alone.

Permission resolution is now answered in-process from one cached select of daas_permissions rows per (policy set, collection); gate, field lists, row filters, validation, and presets all derive from the same rows. Caches use a 30-second coalesced TTL, and writes to any access-control table through the API invalidate them immediately.

Measured after: 2.0 round trips per request for admins and non-admins alike, p50 94–97 ms.

  • PERMISSION_CACHE_TTL_MS tunes the cache TTL (0 disables cross-request reuse)
  • The TTL is the backstop for permission changes made via direct SQL — those can take up to 30 s to propagate; API writes propagate immediately
  • A migration rewrites the permission SQL functions from plpgsql to LANGUAGE sql with identical semantics — they remain the RLS predicates

Performance — Optional local JWT verification

Who is affected: Opt-in via SUPABASE_JWT_SECRET; behavior is unchanged when unset.

Setting SUPABASE_JWT_SECRET lets the server verify HS256 access tokens in-process instead of calling GoTrue’s /auth/v1/user on every request, taking warm requests from 2 Supabase round trips to 1. The fast path applies only to users with an active daas_users row; everyone else falls through to GoTrue, so deleted and banned auth users are still rejected.

Trade-off: for a still-active user, a session revoked at GoTrue (e.g. logout elsewhere) stays valid until token expiry. Leave the variable unset if immediate revocation matters more than the saved round trip.


Fixed — root-scope requests resolved permissions from the unscoped policy set

Who is affected: Multi-tenant deployments using Resource URI scoping.

A request without an X-Resource-Uri header resolves to root scope, but the policy lookup treated root the same as “no scope” and returned the caller’s flat, all-scopes policy set. A user whose only grants were bound to a sub-scope therefore passed permission checks at root — and for scope-enabled collections with inheritance_mode='down', the root read path applies no scope filter, so those requests could read and write across tenants. Root scope now resolves to root assignments plus public policies only, covered by unit tests and the cross-tenant API suite.


Fixed — saved-parent “Add Existing” mass-unlinked existing children (list-o2m)

Who is affected: Forms using the list-o2m interface on saved records.

The relation writer treats an emitted O2M value as the full desired set of children: any already-linked child missing from the array is FK-nullified (or deleted under one_deselect_action='delete'). On a saved parent, “Add Existing” stages only the new link — so linking one item and saving silently wiped every other pre-existing child. The save payload now includes every currently-linked child (fetched live, unpaginated), so the writer has nothing left to deselect. Covered by a regression spec.


Fixed — paginated M2A saves dropped junction rows from other pages (list-m2a)

Who is affected: Forms using the list-m2a interface with more linked items than one page.

Same defect class: the M2A write path deletes every existing junction row and re-inserts the emitted payload, but list-m2a built that payload from the current page only. Staging one new link and saving replaced all junction rows with just the visible page, silently dropping everything on page 2+. The payload is now built from the full junction set with local changes merged on top.


Fixed — permission caches invalidate on nested relational writes

Role changes submitted as a nested payload (e.g. PATCH /api/users/:id with roles) bypassed the invalidation hooks, so the old permissions stayed cached until the TTL expired. The relation writer now invalidates explicitly whenever a write touches an access-control table.


Changed — Buildpad UI interface components synced to 1.10.0

Who is affected: All Platform Studio forms using relational or selection interfaces.

list-o2m, list-m2a, list-m2m, select-dropdown-m2o, and collection-item-dropdown upgraded to @buildpad/ui-interfaces 1.10.0, plus a sweep of upstream audit fixes (ui PRs #95–#122). Highlights:

  • Collection forms and lists no longer 500 when a view includes O2M/M2M/M2A fields (previously requested as bare columns), and list-m2m works with related collections whose primary key isn’t named id
  • list-o2m “Create New” no longer writes the unsaved-parent + placeholder as the child’s foreign key
  • list-m2m: sort corruption across pages fixed, staged creates render on every page, reorder respects the page offset, duplicate initial fetch removed
  • Selection interfaces: no more crashes on choice values that collide after stringification (e.g. 1 vs "1"), allowOther commit and edge-case fixes, icon/color rendering corrections, tree-view UX improvements, duplicate-value handling
  • Relation hooks: stale in-flight fetches can no longer overwrite newer state, and junction rows are updated/deleted by the real junction primary key

Added

  • docs/ACCESS_CONTROL.md — the user → role → policy → permission model, scopes, merge semantics, runtime sequence with measured cost profile, cache inventory, and invalidation rules
  • tests/load-cert — the load-certification harness behind the certified capacity figures, with versioned reports, pnpm cert:* scripts, runbook, and reporting guide
Last updated on