> ## 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>
