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 and column masks, evaluated per query inside an organisation.
Both questions have to be answered. Neither substitutes for the other.