RBAC Architecture & Specs
A deep dive into the Dual-Approach database strategy, deterministic string validation, and the 3-Stage execution pipeline of the Aeion Access Control Engine.
Security & Execution Metrics
RBAC Processing Overhead
<1ms
Permission Syntax
Regex Validated `^(?:\*|[a-z][a-z0-9-]*):(?:\*|[a-z][a-z0-9_-]*)$`
Enforcement Layers
4 (UI, Pre-Query, Post-Read, Pre-Write)
Role Hierarchy
Nested inheritance (cycle-safe, depth-limited)
1. The Dual-Approach Database Strategy
Aeion OS balances the flexibility needed for content against the strict constraints required for enterprise security.
- Flexible Collections (JSONB): Standard modules (Commerce, Content) use the Collection System, storing dynamic fields in a flexible data JSONB column.
- Fixed RBAC Admin APIs: Security tables — roles, members, teams — completely bypass the Collection System. They utilize fixed, typed PostgreSQL columns, enforced by database-level constraints (NOT NULL, primary keys, indexed lookups on every foreign-key-style column) — deliberately without cross-database REFERENCES constraints, since a tenant member row references a user record that may live in a different database — and are accessed via hyper-fast, direct Drizzle Admin APIs (/api/admin/roles). This deliberate architectural choice guarantees maximum authentication performance and ensures a malformed JSON payload can never compromise the security hierarchy.
typescript
// Example: Validating canonical permission strings globally across the OSimport { z } from "zod";import { hasPermission } from "@aeion/types/rbac/permissions";
// 1. Strict regex enforcement during API callsconst PERMISSION_REGEX = /^(?:\*|[a-z][a-z0-9-]_):(?:\*|[a-z][a-z0-9\_-]_)$/;export const zPermission = z.string().regex(PERMISSION_REGEX);
// 2. Wildcard resolution// If required is "finance:payroll", and userPerms includes "finance:_" or "_:*", this returns true.if (!hasPermission(ctx.user.permissions, "finance:payroll")) {throw HttpError.forbidden("Payroll access required.");}2. Dynamic Row-Level Security (RLS)
Instead of forcing developers to write imperative authorization checks in their route handlers, Aeion leverages a declarative AST structure.
- The `createScopedReadAccess` Helper: Developers define an access function on the schema. If the user is an admin, it returns true (granting full access). If the user is standard staff, it returns a strongly-typed Filter object (e.g., { managerId: { equals: user.id } }).
- AST to Drizzle Translation: The database access layer intercepts every find() call, takes the Filter object returned by the RBAC engine, translates it safely into a parameterized Drizzle WHERE clause, and executes the query natively at the database level — no in-app post-filtering, no risk of an unfiltered row slipping through.
Security begins before the data is even requested. During the initial application boot, the OS cross-references the user's permissions against the declared permission requirements of every registered view. If the user is unprivileged, the view is physically purged from the client's navigation tree. This is the "Visual Airlock" — a malicious user cannot interact with a restricted UI because the UI code was never mounted into their browser's memory space.
Beyond the standard action vocabulary (`read`, `write`, `manage`, `admin`, `publish`, `approve`, `export`), a module can declare its own custom sub-scope in the same `:` format — for example `finance:payroll` or `hr:compensation` — to gate a single high-risk feature independently of the module's broader `finance:manage` grant. These custom strings pass through the same regex validation and wildcard resolution as every other permission in the system, so a role holding `finance:*` still covers them without any extra wiring.
Frequently Asked Questions
Security tables — roles, members, teams — run on fixed, strictly-typed PostgreSQL columns with hard database constraints (NOT NULL, primary keys, indexed foreign-key-style lookups) and are read through direct Drizzle Admin APIs like `/api/admin/roles`. That keeps permission checks on a hyper-fast path and guarantees a malformed JSON payload can never corrupt the security hierarchy — the flexibility that content collections need would be a liability here.
Developers define a declarative access function on the schema. It returns `true` for admins or a strongly-typed `Filter` object for standard staff (e.g., `{ managerId: { equals: user.id } }`). The database access layer intercepts every `find()`, translates that Filter into a parameterized Drizzle `WHERE` clause, and runs it natively at the database level — no in-app post-filtering, so an unfiltered row can never slip through.
RBAC processing overhead is under 1ms. Enforcement spans four layers — the client-side Visual Airlock, then the three server stages (Pre-Query filter injection, Post-Read field redaction, and Pre-Write guard) — and the query itself does the filtering work at the database, so total request time stays dominated by the database and network rather than the security checks.
A module can declare custom sub-scopes in the same `<module>:<action>` format — for example `finance:payroll` or `hr:compensation` — to gate a single high-risk feature independently of a broader `finance:manage` grant. These custom strings pass through the identical regex validation and wildcard resolution as every other permission, so a role holding `finance:*` still covers them with no extra wiring.
The Visual Airlock. At application boot the OS cross-references the user's permissions against every registered view's declared requirements and physically purges anything they lack from the client's navigation tree — not hidden with CSS, genuinely never mounted. A restricted UI returns a hard 404 rather than a grayed-out button an attacker could poke at.
See the Access Control Engine in Context
From the client-side airlock down to parameterized WHERE clauses at the database row — the same engine on every request.