Quickstart: Command the Data Layer

Write security-enforced queries, drop into raw Drizzle, configure 3-stage RBAC, time-travel via Temporal, leverage Aegis guardrails, integrate audit logs — the full setup in 6 steps, in under 25 minutes.

1

Execute High-Level Queries

The transparent wrapper exposes a consistent API. Tenant isolation, soft-delete, timestamps, and audit logging happen automatically.

```typescript // Inside any Aeion route handler, MCP tool, or service const { docs, hasMore, cursor } = await ctx.db.find("contacts", { where: { score: { gt: 80 } }, sort: "-lastActivityAt", limit: 10, });

// Lookup by ID const contact = await ctx.db.findById("contacts", "ct_xyz");

// findOne takes a where-filter, not an ID const contactByEmail = await ctx.db.findOne("contacts", { email: "jane@example.com", });

const newContact = await ctx.db.create("contacts", { email: "jane@example.com", firstName: "Jane", });

await ctx.db.update("contacts", "ct_xyz", { status: "active" }); await ctx.db.delete("contacts", "ct_xyz"); ```

What happens automatically:

  • tenantId = ctx.tenant.id injected into every query
  • RBAC filter applied (only return authorized records)
  • Field-level access checks (sensitive fields stripped)
  • Soft-delete filter (only return non-deleted by default)
  • Audit log entry created
  • Temporal event emitted (for replay / time-travel)

Operators (programmatic API — full zFilter set, 30+ operators): comparison (equals, not_equals, gt, gte, lt, lte, between, not_between), string (contains, not_contains, starts_with, ends_with, matches, ilike, search), array (in, not_in, contains_any, overlaps, has_any, has_all), null (is_null, is_not_null, exists), JSON (json_contains, json_path_equals, has_key, has_keys), date (before, after, older_than, newer_than), relational (some, every, none, count), and logical groupings (AND, OR, NOT).

2

Drop into Raw Drizzle for Complex Queries

For complex aggregations, joins, or raw SQL, use the same proxy — it routes directly to Drizzle.

typescript // Complex aggregation const stats = await ctx.db .select({ region: schema.crm_contacts.region, count: sql<number>count(*)::int, avgScore: sql<number>avg(${schema.crm_contacts.score})`, }) .from(schema.crm_contacts) .groupBy(schema.crm_contacts.region);

// Join const dealsWithContacts = await ctx.db .select() .from(schema.crm_deals) .leftJoin( schema.crm_contacts, eq(schema.crm_deals.contactId, schema.crm_contacts.id), ) .where(eq(schema.crm_deals.stage, "active"));

// Raw SQL (parameterized) const result = await ctx.db.execute(sql SELECT COUNT(*) FROM ${schema.crm_contacts} WHERE created_at > NOW() - INTERVAL '30 days' ); ```

Multi-tenant safety preserved — even in raw Drizzle, the wrapper enforces tenant isolation. Cross-tenant queries from a request context return an explicit error.

Type-safe end-to-end via Drizzle's schema types.

3

Configure 3-Stage RBAC

Define security on your collection schema:

```typescript import { createAccessControl, createScopedReadAccess, hasAnyRole, } from "@aeion/core/schema/security/accessControlHelpers"; import type { CollectionConfig } from "@aeion/types/schema";

export const HighSecurityAssets: CollectionConfig = { slug: "secure_assets", access: createAccessControl({ // STAGE 1 — Pre-Write Gate create: ["admin"], update: ["admin", "asset-manager"], delete: ["admin"],

// STAGE 2 — Pre-Query Gate (Row-Level Security) read: createScopedReadAccess(async (ctx, user) => { if (hasAnyRole(user, ["admin"])) return true; return { ownerId: { equals: user.id } }; }), }), fields: [ { name: "title", type: "text" }, { name: "ownerId", type: "relationship", relationTo: "users" }, { name: "financialValuation", type: "number", access: { // STAGE 3 — Post-Read Masking read: ({ user }) => hasAnyRole(user, ["admin", "finance-manager"]), }, }, ], }; ```

3-stage enforcement on every API call:

  1. Pre-Write: mutations rejected if user lacks permission
  2. Pre-Query (RLS): every read has user's filter injected before SQL
  3. Post-Read: sensitive field values stripped from response payload

Defense in depth — even if one stage is bypassed, others catch the attempt.

4

Time Travel with Temporal Queries

Every mutation captured via temporal events. Replay any past state.

```typescript import { reconstructAtTime, getDocumentHistory, getDocumentTimeline, compareAtTimes, } from "@aeion/core/temporal";

// Get the event-by-event history of a document const history = await getDocumentHistory({ ctx, collection: "pages", id: "page_123", });

// Reconstruct the document as it existed at a specific moment const pageYesterday = await reconstructAtTime({ ctx, collection: "pages", id: "page_123", asOf: "2026-05-19T18:00:00Z", });

// Compare two points in time const diff = await compareAtTimes({ ctx, collection: "pages", id: "page_123", fromTime: "2026-05-19T00:00:00Z", toTime: "2026-05-20T00:00:00Z", });

// Restoring a document to a past state is a write — pass the reconstructed // state to ctx.db.update(). The temporal layer captures the restore as a // new event so the original timeline stays auditable. await ctx.db.update("pages", "page_123", pageYesterday.doc); ```

Use cases:

  • "I accidentally overwrote..." — UX rescue
  • Debugging — "What did this look like 2 hours ago?"
  • Compliance / forensic — "What did the user see at dispute time?"
  • Replay testing — replay 1 day of mutations against staging

Storage — RFC 6902 JSON Patches (compact). Typical overhead: 2-5% of base table size. Retention configurable per collection.

Aegis integration — temporal events feed cross-collection rollback. "Restore my tenant to last Tuesday" works because every mutation is replayable.

5

Leverage Aegis Safety Guardrails

Aegis intercepts dangerous database operations:

Protected against:

  • DROP TABLE / TRUNCATE / DELETE on tables with real data
  • ALTER TABLE that destroys columns
  • UPDATE WHERE with no proper filter

Pre-flight check on `bun run db:sync`:

```bash $ bun run db:sync

🚨 Aegis Guardrail: This migration would DROP table crm_contacts (currently has 247,891 rows) To proceed, run with --force-destructive flag. Aborting. ```

Force-destructive mode:

```bash $ bun run db:sync --force-destructive

🚨 Aegis Guardrail: Creating safety snapshot before destructive migration... ✓ Snapshot created: aegis_snapshots/2026-05-20T15-30-00.sql.gz (24 MB) ✓ Proceeding with migration... ✓ Migration complete. Snapshot retained for 90 days. ```

Rollback any migration via Admin → Aegis → Rollback Migration. Pre-migration snapshots retained for 90 days.

Run-anywhere safety — same checks in dev / staging / production. No surprise behavior.

6

Integrate with the Audit Log

Every data access automatically logged.

Default fields captured:

  • User identity, tenant ID, operation type
  • Collection + record ID
  • Field-level details (which fields read / changed)
  • IP, device fingerprint, HTTP method, endpoint
  • Latency, result size, outcome

Compliance tagging:

typescript export const PaymentMethods: CollectionConfig = { slug: "payment_methods", governance: { auditLevel: "high", complianceFrameworks: ["pci", "gdpr"], retentionDays: 365, }, };

Query the audit log — it's a regular collection (slug audit_log), queried via the standard service registry:

```typescript // "Who accessed customer ct_xyz in the last 24 hours?" const entries = await ctx.services["audit_log"].list({ ctx, where: { resourceCollection: { equals: "contacts" }, resourceId: { equals: "ct_xyz" }, createdAt: { newer_than: "24h" }, }, limit: 100, });

// "Show all data exports by user u_admin in the last 30 days" const exports = await ctx.services["audit_log"].list({ ctx, where: { actorId: { equals: "u_admin" }, action: { equals: "export" }, createdAt: { newer_than: "30d" }, }, }); ```

SIEM integration — push to Splunk / DataDog / Sumo Logic / Elastic via streaming audit-log connector.

Integration Recipes

RBAC + Aegis stack. RBAC prevents unauthorized data access; Aegis prevents accidental data destruction. Defense-in-depth.

Temporal + Aegis for full rollback. Temporal events replay mutations; Aegis snapshots restore schema state. Together: "restore my tenant to last Tuesday with full schema integrity."

Field-level RBAC + audit log. Sensitive fields stripped via field-level RBAC. Access attempts log even when blocked — useful for detecting probing.

Cross-tenant migrations. Platform operations use sysDb — separate non-tenant-scoped client only accessible to platform-tenant code. Audit log auto-tags these as platform-level.

Temporal + Forge ADR. Architectural decisions logged as Forge ADRs. Temporal events provide replay history; ADRs provide the why.

Performance-critical queries. Use the Drizzle escape hatch for exact SQL control. The wrapper still injects tenantId and logs the access.

Troubleshooting

Query returns no results but data exists. RBAC filter excluding the records. Check user's roles + collection's read filter. Use Admin → Inspect → Query with admin token to verify data exists; debug the RBAC rule.

'Cross-tenant access denied'. Querying data from a different tenant in request context. Move to platform-tenant code using sysDb. Or verify the user has cross-tenant scope.

Temporal events bloating storage. Default retention 90 days. For high-write collections, reduce to 30 days. Or disable temporal tracking entirely (set temporalTracking: false in schema).

Aegis blocking migration. Aegis correctly identifying destructive change. Verify intent. If correct, run with --force-destructive — safety snapshot created automatically.

RBAC scoped filter slow. Inspect SQL via Admin → Inspect → Query Trace. Add database indexes for the columns referenced in scoped-read filters.

Audit log too verbose. Default captures all operations. Set governance.auditLevel: "low" on collections that don't need verbose logging.

Frequently Asked Questions

No. JavaScript Proxy adds ~0.001ms per operation — undetectable when network/disk I/O is 10ms-100ms. Massive developer experience gain: zero boilerplate for tenant isolation + RBAC + audit logging.

Internal safety mechanism for database migrations. If `bun run db:sync` would drop a table with real data, Aegis intercepts, throws a critical warning, demands `--force-destructive`, and executes a pg_dump S3 snapshot before complying.

Database logic enforces rules; admins don't write code for routine changes. The visual role manager in the Admin UI lets them construct permission matrices visually, which feed directly into query-time enforcement.

Yes — raw Drizzle escape hatch always available. The wrapper still injects tenantId and audit logs the operation; you get full SQL flexibility. For genuine bypass (cross-tenant migrations), use `sysDb` from platform-tenant code.

Auto-population for one-to-one and one-to-many. For many-to-many or complex queries, drop into raw Drizzle. Filter AST supports relationship filters: `{ company: { equals: "comp_xyz" } }` joins to the related table.

Typical 2-5% of base table size. High-write collections can configure shorter retention or exclude entirely. RFC 6902 JSON Patches are compact + replayable.

Yes — Drizzle is your ORM. Export schema definitions, run migrations against your new DB. Tenant isolation + RBAC would need to be reimplemented elsewhere, but the data is portable.

Aeion supports PostgreSQL read replicas + streaming replication. Read-only queries auto-route to replicas; writes go to primary. Failover handled by your cloud provider (RDS, Aurora, etc.).

Yes, per migration — pass `--force-destructive` to `bun run db:sync` (or set `AEGIS_FORCE_DESTRUCTIVE=1`) to bypass the guard for that run; it still takes an automatic pre-migration snapshot first. There's no environment-wide "disable Aegis" switch — the guard is on by default everywhere, dev included.

Mutations succeed regardless — temporal capture is best-effort + async. Failures log to system audit; ops alerted. Mutation not rolled back (preserves data integrity); event can be reconstructed from the audit log if needed.

Ready to command the data layer?