npm run bench in alfiz-ts): synthetic organizations against the in-memory driver on Node 22. Treat the shapes as the result and the absolute microseconds as one machine’s afternoon; rerun on your hardware and your driver before capacity planning.
What is measured
- Cold closure + check. The expensive path, being a principal’s first check in a process. One closure supply (groups walked, roles resolved, rows fetched) plus one scoped evaluation against a depth-20 hierarchy.
- Warm check. The common path: cached closures, in-memory evaluation, no driver contact.
Measured (memory driver, Node 22)
How to read this
The memory driver scans; a database driver indexes. Every closure fetch against the memory driver filters the full grant table per subject, so cold-miss numbers scale with total rows and are an upper bound for an indexed store, where the same fetch pays per matching row (@@index([subject]) is in the shipped schema). The 10⁶ cold numbers are the scan cost, not the algebra’s.
Warm checks transfer directly, because they never touch the driver. What they scale with is the number of rows in the principal’s own closure: the 200-group chain at 10⁶ rows puts ~10⁵ rows in one closure, and that is what the 5.8 ms warm p50 measures, rather than total table size. A realistic principal in that scenario (20 groups, dozens of rows) checks in the 10⁴-shape’s double-digit microseconds. The design consequence: deep group chains with heavy per-group grants are the shape to avoid, and roles (one row, many patterns) are the tool that avoids them.
Hierarchy depth is cheap. Depth-20 object chains are one resolver walk on a miss and cached thereafter; depth contributes O(depth) string comparisons per check.
The epoch adds one single-row read per revalidation window per process, a constant cost independent of every size above. See Caching & staleness.