There are three common models, from strongest to cheapest:
- A database per tenant: the strongest isolation. But migrations, backups and connection counts grow with every customer.
- A schema per tenant: one database, with one Postgres schema per tenant. It sits in the middle.
- Shared tables with a
tenant_idcolumn: the cheapest and most common. It is also the weakest by default, because one missingWHERE tenant_id = ...leaks data.
Controls for the shared model:
- Take the tenant id from the authenticated session or token. Never take it from a body field or header that the client chooses.
- Scope queries in one place. A repository layer or ORM middleware should add the tenant filter, so no handler has to remember it.
- Add a database backstop such as Postgres Row-Level Security (RLS).
- Put
tenant_idin unique keys and foreign keys, so a row can't point at another tenant's row. - Scope every other store too: cache keys, file storage paths, search indexes, queues and exports.
- Test it. Write automated tests where a user of tenant A asks for tenant B's ids and gets a 404.
Also add per-tenant rate limits, so one busy customer can't slow down the others. This is the noisy neighbour problem.
- The models are a database per tenant, a schema per tenant, or shared tables with
tenant_id. - The tenant id must come from the authenticated identity, never from client input.
- Add the tenant filter in one shared layer, with RLS as a database backstop.
- Caches, files, search indexes and queues need tenant scoping too.
- Automated cross-tenant tests prove that isolation holds.