# Tenant isolation

> Schema-per-organisation instead of a discriminator column: the schema is set on the connection for the transaction, so a query that forgets everything still cannot cross an organisation boundary

Source: https://aipril.dev/docs/platform/access/tenant-isolation

## Schema per organisation

Each organisation's tables live in **their own database schema**. Access is routed by setting the
schema for the duration of the transaction, and per-organisation data is only ever reached through
that path.

The process service follows the same model with its own schemas per organisation.

## Why not a discriminator column

The common alternative puts a `tenant_id` on shared tables and filters on it.

Compare the failure modes:

| | Discriminator column | Schema per organisation |
|---|---|---|
| Isolation holds because | Every query in the codebase remembers the filter | The objects are not in the search path |
| A forgotten filter | Returns another organisation's rows | Returns nothing - the table is not visible |
| A new developer's first query | Is a risk | Is not |
| Review burden | Every query, forever | The routing helper, once |

The property worth stating plainly: **isolation does not depend on discipline.** It is not that people
here are more careful; it is that carelessness has a different outcome.

The trade-off: schema-per-organisation means migrations run per organisation, which is real
operational work. That is the side we chose.

## Beyond the database

Isolation is not only a storage property. Per organisation:

- **Object storage** - analytical files are scoped, and bucket scope is enforced, not assumed
- **Model providers and keys**, including an organisation's own
- **Knowledge, embeddings, custom skills and installed marketplace skills**
- **Credits, budgets and suspension state**
- **The audit trail**
- **Application repositories and their installations**
- **AI employees**, each a subject inside one organisation

## And underneath the rest of access control

Isolation answers "which organisation". It does not answer "which rows" or "which values" - those are
[row policies](https://aipril.dev/docs/platform/access/row-level-security.md) and
[column masks](https://aipril.dev/docs/platform/access/column-masking.md), evaluated per query inside an organisation.

Both questions have to be answered. Neither substitutes for the other.
