> ## Documentation Index
> Fetch the complete documentation index at: https://docs.iru.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Multi-tenant isolation

> How Iru keeps every organization's data separate - enforced in the database engine with row-level security - and every change tamper-evident.

Iru Identity is multi-tenant: many organizations share the same service. The most
important property of that design is that **one tenant can never see or touch
another's data**. Iru enforces this in the database engine itself, not just in
application code - so even a bug in a query cannot cross the boundary.

## Isolation enforced by the database

Every table that holds tenant data carries a `tenant_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 their `tenant_id` matches the tenant of the
  current request.

```sql theme={null}
-- The shape every tenant table shares
ALTER TABLE app.<table> ENABLE ROW LEVEL SECURITY;
ALTER TABLE app.<table> FORCE  ROW LEVEL SECURITY;

CREATE POLICY tenant_access_policy ON app.<table> FOR ALL
  USING      (tenant_id = current_setting('iru.tenant_id'))
  WITH CHECK (tenant_id = current_setting('iru.tenant_id'));
```

This is applied uniformly and at scale - **hundreds of tables**, each with forced
RLS and an identical read-and-write policy - not just to a privileged few.

<Note>
  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.
</Note>

## The tenant context travels with every request

```mermaid theme={null}
flowchart LR
  req["Request to<br/>tenant.subdomain"] --> mw["Resolve tenant<br/>(reject if unknown)"]
  mw --> tx["Open DB transaction"]
  tx --> setl["SET LOCAL iru.tenant_id = …<br/>+ instigator + correlation id"]
  setl --> rls["RLS policy reads<br/>the same setting"]
  rls --> data[("Only this tenant's rows")]
```

1. 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**.
2. 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.
3. 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.

<AccordionGroup>
  <Accordion title="What's captured" icon="clipboard-list">
    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.
  </Accordion>

  <Accordion title="Why it's append-only" icon="lock">
    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.
  </Accordion>

  <Accordion title="It can't silently drift" icon="circle-check">
    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.
  </Accordion>
</AccordionGroup>

## 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](/en/identity/security/platform-architecture)).
Within that database, RLS keeps each tenant separate.

<CardGroup cols={2}>
  <Card title="Platform architecture" icon="server" href="/en/identity/security/platform-architecture">
    The regions, planes, and infrastructure this isolation runs on.
  </Card>

  <Card title="Authentication architecture" icon="signature" href="/en/identity/security/authentication-architecture">
    How sign-in and API requests are made phishing-resistant and tamper-proof.
  </Card>

  <Card title="Activity log" icon="list-timeline" href="/en/identity/administration/activity-log">
    The audit trail, surfaced for admins to review.
  </Card>

  <Card title="Security & privacy" icon="lock" href="/en/identity/security/security-and-privacy">
    The guarantees these mechanisms add up to.
  </Card>
</CardGroup>
