The Unbreakable Data Foundation.
Stop struggling with raw ORMs and brittle database security policies. Aeion OS shields its massive PostgreSQL infrastructure behind a Transparent Database Wrapper powered by Drizzle. It natively enforces 3-Stage RBAC security, provides "Holographic" Temporal Time Travel to query historical data, and ensures zero-data-loss via the Aegis backup system.
Enterprise Data Orchestration
Transparent Drizzle Proxy
Aeion utilizes a sophisticated JavaScript Proxy architecture. Developers simply call `ctx.db.find()`, and the wrapper intelligently routes the call to the high-level Collection API (enforcing security) or falls through to raw Drizzle ORM for low-level queries seamlessly, eradicating cognitive load.
Holographic Temporal State
Don't just save data; save history. The wrapper intercepts database mutations and generates RFC 6902 JSON patches. You can rewind time to query the exact state of a Commerce Product or a CMS Article exactly as it existed on March 4th at 2:00 PM.
Aegis Backup & PITR
Database destruction is virtually impossible. The Aegis guardrail prevents administrators from dropping non-empty tables. Furthermore, Aegis integrates with `pgBackRest` and MinIO/S3 to provide Point-In-Time Recovery (PITR), guaranteeing absolute data safety.
3-Stage RBAC Security
Access control isn't an afterthought; it's a kernel-level interceptor. The database wrapper enforces security across three distinct stages: Pre-Query (Row-Level Security), Post-Read (Field-Level Redaction), and Pre-Write (Validation), ensuring users never see or modify unauthorized data.
Automated Audit Blame
In enterprise software, you must know who changed what. The database wrapper intercepts all `CREATE` and `UPDATE` commands, forcefully injecting the authenticated user's `id` into the `updatedBy` fields, overwriting any spoofed API payloads.
Zero-Downtime Migrations
Forget maintenance windows. The schema engine parses new collections into memory, dynamically generates the Drizzle `pgTable` definitions at boot, and mounts them without taking the OS offline.
Three Tenant Isolation Modes — One API
COLUMN mode (default)
All tenants share the same Postgres tables; the tenant ID is auto-injected into every WHERE/INSERT/UPDATE/DELETE clause. Cheapest to operate, most efficient for SMB SaaS workloads with many tenants. The tenant predicate is enforced _before_ the query reaches the database — developers cannot construct a cross-tenant query even by mistake.
SCHEMA mode
Each tenant gets a dedicated Postgres schema. The connection's search path is set per-tenant automatically. Used for compliance-sensitive workloads (HIPAA covered entities, EU data residency) that demand stronger physical isolation than column scoping. Same TypeScript API.
DATABASE mode
Each tenant gets a dedicated Postgres database, handed out from a per-tenant connection pool. Used for white-label enterprise deployments that contractually demand "your data lives on your dedicated infrastructure." Same TypeScript API.
The Temporal Subsystem
Point-in-time reconstruction
Rebuild any document at any past timestamp by replaying reverse patches newest-to-oldest.
Time-travel HTTP API
Dedicated endpoints reconstruct, diff, or walk the full timeline of any tenant-scoped document.
`.asOf(timestamp)` queries
A query-builder modifier that fetches the world as it was, inline with your normal query pipeline — no separate history API to learn.
Hot to archive tiering
Events older than 30 days offload to cold storage automatically; queries transparently span both tiers so old history is never actually out of reach.
Retention enforcement
Per-collection retention windows, with a nightly purge of expired events that's itself audited.
Mutation recording
Reverse-patch capture wired into every collection's write path, so nothing has to opt in per-feature to get history.
The 3-Stage RBAC Pipeline
Every database call routes through three intercept points. Security is NOT optional — there's no "raw query" escape hatch in application code.
Stage 1 — Pre-Query (Row-Level Security). Before the query hits Postgres, the wrapper invokes the collection's access.read() access function with the authenticated user. The function returns either true (full access), false (no access — returns 404 to avoid leaking row existence), or a Filter object (e.g., { ownerId: { equals: user.id } }) that gets _intersected_ with the developer's query predicate. Developers cannot bypass this — the wrapper mutates the query before Drizzle compiles it.
Stage 2 — Post-Read (Field-Level Redaction). After Postgres returns rows, the wrapper iterates fields and consults the per-field read guards. Fields marked access.read: ({ user, doc }) => ... are stripped from the response if the guard returns false. A user without view:salary permission gets the row back with salary: undefined — never even sees the field name in the JSON.
Stage 3 — Pre-Write (Validation + Audit Injection). On create / update / delete, the wrapper enforces access.create / access.update / access.delete guards, then runs Zod validation per field, then forcefully injects createdBy: user.id / updatedBy: user.id / updatedAt: now() — overwriting any spoofed values in the request body. The actual write goes through Drizzle with the sanitized payload.
The 3-stage pipeline is fail-safe by default: a collection with no access config gets access: { read: false, ... } — no access — until an author explicitly opens it up. Security is opt-IN, not opt-out.
Time-Travel Queries Inline
```typescript // Read the live state const post = await ctx.services["posts"] .query(ctx) .where({ id: "post_abc" }) .first();
// Read the state as of last Tuesday const lastTuesday = await ctx.services["posts"] .query(ctx) .where({ id: "post_abc" }) .asOf("2026-05-12T10:30:00Z") .first();
// Compare two timestamps const diff = await fetch( "/api/v1/temporal/compare?collection=posts&id=post_abc&a=2026-05-12T10:30:00Z&b=2026-05-15T14:00:00Z", ); // → { added: { tags: ["new"] }, changed: { title: { from: "...", to: "..." } } } ```
Each .asOf() call is implemented as: (1) find the latest version of the document at-or-before the target timestamp, (2) apply reverse JSON Patches newest→oldest to reconstruct that historical state. Sub-100ms typical latency on a healthy event log.
asOf() works for .first(), .findMany(), .count(), and .exists(). Filters apply against the historical state, so "what contacts did we have on March 1?" is a single query.
What the Wrapper Won't Let You Do
Subtitle: The Wrapper structurally forbids the operations that have caused real incidents in real SaaS products.
- Raw SQL without tenant scope — Application code can't construct a query string. The Wrapper's
db.find/create/update/deleteAPI is the only path, and it auto-injects tenant scope. A raw escape hatch exists for the platform's own internal subsystems, but it's never exposed to module code — the lesson that led to this rule is the same one behind the Aegis backup system below. - DROP TABLE / DROP COLUMN on non-empty tables — Layer 1 of Aegis runs _before_ Drizzle's migration applies. Row counts are checked; non-empty data refuses the migration unless
--force-destructiveis set. - Spoof the audit trail —
createdBy,updatedBy,updatedAtare overwritten on every write. Even if a developer's API handler accidentally accepted these from the request body, the Wrapper rewrites them. - Forget to log a destructive action — Every CRUD operation auto-emits a
record.created/record.updated/record.deletedevent with the authenticated user, the previous state, and the patch. Notifications + workflows + temporal events all subscribe to this stream. - Skip the Zod validation — Every field declared on a CollectionConfig has a generated Zod schema. The Wrapper runs it before any write — no "I'll just trust the request body" path exists.
The point is structural impossibility, not policy. Security policies drift; structural constraints can't.
Frequently Asked Questions
It refuses them in application code. System-level operations (cross-tenant migrations, platform admin dashboards) use the `sysDb` client directly — a separate non-tenant-scoped surface only accessible to platform-tenant code. Module code can't import it. The TypeScript types enforce the boundary: `AeionContext.db` is the tenant-scoped wrapper, `AeionContext.sysDb` is `unknown` outside platform-tenant code paths.
Stage 1 adds the access function's filter to the WHERE clause — typically a single boolean conjunction, ~zero query-plan impact. Stage 2 runs in TypeScript over the result set — measured at ~3-5 μs per row per guarded field. Stage 3 runs Zod validation, which is ~5-20 μs per write. Total wrapper overhead is dominated by network + Postgres, not the interceptors.
From application code, no. Module routes get a `ctx.db` that is the tenant-scoped wrapper, period. A raw escape hatch exists deep in the platform's internals for the platform's own subsystems (the temporal recorder, the migration engine, Aegis) that legitimately need bypass. Your collection code does not have access to this escape hatch, by design.
The `afterChange` / `afterDelete` hooks run *after* the primary write commits, in the same transaction. The reverse-patch generation is a memory-only diff (RFC 6902) and a single INSERT into `temporal_events` — typically <2 ms on modern hardware. For very high-write workloads (>1000 TPS), enable the async-archive option which buffers events through Redis before flushing.
Roughly proportional to your write volume × retention window. A typical SMB tenant with 10K-100K records sees ~1-5 GB/month. Tier-2 archive offloads events older than 30 days to S3 as compressed JSONL — about 90% storage savings. `.asOf()` queries against archived data transparently fetch + decompress on demand (slower than live events but still sub-second).
Transactions: yes, the Wrapper accepts a transaction handle and threads it through. Views: read-only views are queryable via the wrapper; materialized views run outside the wrapper since the data is denormalized at refresh time. Custom Postgres extensions (pgvector, PostGIS, pg_trgm): supported — the wrapper passes through unknown operators to Drizzle.
No — and you shouldn't want to. Every measured workload we've profiled has shown that the security + audit overhead is far less than the cost of the underlying query. If you genuinely need to bypass (e.g., for a million-row analytical aggregation), use the `sysDb` admin-side or build a materialized view + scheduled refresh.
Yes. BullMQ workers receive a primitive payload + tenant ID, reconstruct the AeionContext via `api.createContext(tenantId)`, and write through the Wrapper exactly like sync code. The reconstruction step also re-applies the 3-stage RBAC against the system user identity that owns the job. There's no async path that escapes the Wrapper.