Isolation enforced by the database
Every table that holds tenant data carries atenant_id and is protected by
PostgreSQL row-level security (RLS):
- The table has RLS enabled and forced (
FORCE ROW LEVEL SECURITY), so the policy applies to every role - there is no privileged path around it. - A single policy governs both reads and writes: rows are visible (
USING) and writable (WITH CHECK) only when theirtenant_idmatches the tenant of the current request.
Because the boundary lives in the database, a query that forgets to filter by
tenant simply returns nothing from another tenant - the engine refuses to show
or change rows outside the active tenant.
The tenant context travels with every request
- The tenant is resolved from the request’s subdomain in middleware. If it can’t be resolved, the request is rejected before it ever reaches a handler.
- For each database transaction, Iru sets the tenant - along with the acting
user and a correlation/trace id - as a transaction-scoped setting
(
SET LOCAL). Because it’s scoped to the transaction, it cannot leak to the next request that reuses a pooled connection. - The RLS policy reads that exact setting, so the security boundary is the same value the application set, established transactionally on every connection.
Least privilege, so audit history can’t be rewritten
The database role the application runs as can read and write live data, but it holds only read access to the audit schema. Even if application SQL were somehow abused, it has no grant to modify or delete audit history - the trail is protected at the privilege layer, independent of any application logic. Referential integrity is tenant-scoped too: foreign keys are composite (tenant_id + id), so a record can only ever reference another record in the
same tenant.
Every change is journaled - append-only
Iru keeps a complete, tamper-evident history of changes to tenant data.What's captured
What's captured
Every mutable table has a mirror audit (journal) table written by a
database trigger on insert, update, and delete. Each entry records what
happened (the action), who (both the acting database principal and the
authenticated user), when, a correlation/trace id tying it to the
originating request, and the full before/after state of the row.
Why it's append-only
Why it's append-only
The trail is protected three independent ways: the trigger only ever inserts
journal rows; the application’s database role has read-only access to the
audit schema; and the trigger runs with definer rights. There is no path for
the application to alter or erase an audit record.
It can't silently drift
It can't silently drift
A standing consistency check compares every live table’s columns against its
audit table and fails if any column is missing - so new fields can’t quietly
escape the audit trail. Schema migrations are themselves versioned,
checksum-verified, and journaled.
Sensitive fields get an extra layer
On top of isolation, particularly sensitive values are encrypted at the field level with AES-256-GCM (authenticated encryption) before they’re stored, and decrypted only when read by the application. Encryption keys are supplied at runtime from a managed secret store, never from code or the database.How this fits the bigger picture
Isolation works hand in hand with regional data residency: each region has its own database, and a tenant’s queries only ever run against its region’s database (see Platform architecture). Within that database, RLS keeps each tenant separate.Platform architecture
The regions, planes, and infrastructure this isolation runs on.
Authentication architecture
How sign-in and API requests are made phishing-resistant and tamper-proof.
Activity log
The audit trail, surfaced for admins to review.
Security & privacy
The guarantees these mechanisms add up to.