Multi-tenancy · How it works
18 min read- How it works
- The spectrum in one picture
- 1. Pool (shared schema + tenant column)
- 2. Pool + database row-level security (RLS)
- 3. Bridge (schema per tenant)
- 4. Silo (database per tenant + tenant catalogue)
- 5. Full-stack silo (Deployment Stamps)
- 6. Tiered pool + silo
- 7. Tenant sharding (distributed database keyed by tenant)
- Comparing the models
- Choosing a model
- Resolving the active tenant
- Tenant wall plus grants: two layers
How it works
The spectrum in one picture
Left is cheapest and weakest, right is strongest and most expensive. Two further models, tiered and tenant sharding, are not points on the line but ways of mixing it.
1. Pool (shared schema + tenant column)
In plain words: everyone's rows live in the same tables, and each row carries a tenant_id saying whose it is.
- Where the wall is: in application code — every query must add
WHERE tenant_id = ?. - Pros: cheapest per tenant; one schema to migrate; onboarding a tenant is an
INSERT; cross-tenant analytics and cross-tenant users are easy. - Cons: one missed filter leaks data (the wall is only as strong as the least careful query); noisy neighbours share one database; per-tenant backup/restore is hard; a large tenant skews indexes and statistics for everyone.
- How the filter is made unforgettable: an ORM global query filter (EF Core
HasQueryFilter, Hibernate filters, a Django default manager) adds the predicate to every query built through the ORM. Raw SQL, bulk jobs and reporting tools bypass it, which is why RLS (model 2) is the usual second step. - Who uses it: the default for most B2B SaaS; Salesforce's architecture docs describe one shared database and one schema for all orgs, every row scoped by an org id.
- Sources: AWS SaaS Lens — silo, pool and bridge models · Azure — multitenant SaaS database tenancy patterns · EF Core — global query filters, multi-tenancy example · Azure — storage and data approaches for multitenant solutions · Citus — multi-tenant apps · Salesforce — platform multitenant architecture
A: A missing tenant filter. In the pool model the only thing separating one customer's rows from another's is a WHERE tenant_id = ? that every query, report, background job and ad-hoc script has to remember. One forgotten predicate — in a new endpoint, a raw SQL report, an admin export — returns another tenant's data, and nothing in the database objects because as far as it knows the query is legal. That is why the pool model is normally paired with a mechanism that makes the filter impossible to forget: a repository or ORM global filter that injects it, and ideally row-level security in the database as a second, independent wall.
2. Pool + database row-level security (RLS)
In plain words: still shared tables, but the database itself hides rows that do not belong to the current tenant, even if the query forgets to filter.
- Where the wall is: in the database engine — a policy such as
USING (tenant_id = current_setting('app.tenant_id')::uuid)is applied to every statement on the table. - Pros: defence in depth — the application filter and the database policy must both fail for a leak; works for ad-hoc SQL and reporting tools too; keeps pool economics.
- Cons: the tenant context must be set on every connection (pooled connections must reset it, or a request inherits the previous tenant); superusers and table owners bypass RLS unless
FORCE ROW LEVEL SECURITYis set; policies add planner work and can hide index use if written carelessly; still one database for noisy neighbours. - Who uses it: Postgres-backed SaaS generally; Nile builds its tenant-aware Postgres around it; Supabase exposes it as the primary access-control mechanism.
- Sources: PostgreSQL docs — Row Security Policies · AWS — Multi-tenant data isolation with PostgreSQL RLS · SQL Server — Row-Level Security (multi-tenant example) · Nile — multi-tenant RLS
A: Through the tenant context rather than the policy. RLS policies usually read the tenant from a session variable, and connection pools reuse sessions. If a request sets app.tenant_id and the connection goes back to the pool without it being reset — or if it was set with session scope instead of transaction scope (SET LOCAL) — the next request on that connection runs as the previous tenant, and the policy faithfully shows it that tenant's rows. The other classic gap is privilege: the table owner and superusers bypass RLS by default, so if the application connects as the owner the policy never applies. The fixes are to set the context per transaction, connect as a non-owner role, and use FORCE ROW LEVEL SECURITY.
3. Bridge (schema per tenant)
In plain words: one database, but each tenant gets its own copy of the tables in its own schema (namespace).
- Where the wall is: in the schema — the connection's
search_path(or a schema prefix) points at one tenant's tables. - Pros: a forgotten
WHEREcannot cross tenants; per-tenant backup, export and deletion are easy (dump or drop one schema); a tenant can be moved to its own database later. - Cons: migrations run N times and can half-fail, leaving tenants on different schema versions; catalogue size grows with tenants × tables (thousands of schemas strain Postgres metadata and tooling); cross-tenant reporting needs a union over every schema; a user or integration that spans two tenants needs two
search_pathswitches and an application-side merge; connection pooling per schema is awkward. - Who uses it: common in Rails and Django SaaS (the
apartmentanddjango-tenantslibraries); Influitive documented running its advocacy platform this way. - Sources: AWS SaaS Lens — bridge model · django-tenants documentation · PostgreSQL — Schemas · Apartment gem README · Crunchy Data — designing Postgres for multi-tenancy · AWS Prescriptive Guidance — Postgres SaaS decision matrix
A: Because the cost is in the catalogue and the operations, not the data. Every schema copies every table and index, so a hundred tables across five thousand tenants is half a million relations in the system catalogue, which slows planning, pg_dump, monitoring queries and anything that lists objects. Migrations become a loop over five thousand schemas that takes hours and can fail part-way, leaving tenants on different versions that the application must tolerate. The model is attractive for tens or hundreds of tenants who value clean per-tenant export and deletion, and painful beyond that.
4. Silo (database per tenant + tenant catalogue)
In plain words: every tenant gets its own database, and a small shared "catalogue" maps each tenant to where its database lives.
- Where the wall is: at the database boundary — a request for tenant A physically connects to A's database.
- Pros: strong isolation, easy to explain to auditors; per-tenant backup, restore, encryption keys, region and sizing; no noisy neighbours at the data layer; a tenant can be deleted by dropping its database.
- Cons: cost per tenant (each database has a floor price); fleet operations — migrations, monitoring and upgrades across hundreds of databases; cross-tenant queries and users who belong to several tenants need a separate layer; the catalogue becomes a critical dependency; every database carries its own connections and memory, so on a small server the cost grows with the tenant count, not the data.
- Who uses it: enterprise and regulated SaaS; Azure SQL elastic pools and the "database per tenant" pattern are built for it. Atlassian's engineering blog describes Jira running one PostgreSQL database per tenant, millions of databases across a few thousand database instances.
- Sources: Azure — database-per-tenant pattern · AWS SaaS Lens — silo model · Azure — tenancy models, horizontally partitioned deployments · Atlassian — migrating Jira's database platform
A: It is the small shared store that maps a tenant id to its database location — server, database name, region, sometimes the schema version and status. Every request resolves the tenant (from the token or the host name), looks it up in the catalogue, and opens a connection to that database. It is critical because it is on every request's path and is the one piece that is not isolated: if it is down no tenant can be served, and if it is wrong a tenant is routed to someone else's data. So it is cached aggressively, kept highly available, and changed only through a controlled onboarding and migration process.
5. Full-stack silo (Deployment Stamps)
In plain words: each tenant, or small group of tenants, gets its own complete copy of the application — compute, database, queues — called a stamp or cell.
- Where the wall is: at the infrastructure boundary; tenants share nothing but the control plane and the routing layer.
- Pros: maximum isolation, including performance and blast radius (a bad deploy or overload hits one stamp); data residency by placing stamps in regions; scales out by adding stamps rather than growing one giant system.
- Cons: highest cost; requires mature automation (infrastructure as code, fleet deploys, per-stamp monitoring); rollouts across stamps take time and can drift; cross-stamp features are hard, and an integration that serves several tenants needs a separate login per stamp.
- Who uses it: ServiceNow's multi-instance model gives each customer dedicated application nodes and a dedicated database; Atlassian Cloud and many hyperscale services run cell-based architectures; the Deployment Stamps pattern is documented by Microsoft, cells by AWS.
- Sources: Azure Architecture Center — Deployment Stamps pattern · AWS — Reducing the scope of impact with cell-based architecture · Azure — tenancy models, automated single-tenant deployments
A: Isolation above the data layer. With a database per tenant, tenants still share application servers, queues, caches and the deploy pipeline, so a runaway tenant can exhaust shared compute and a bad release breaks everyone at once. A stamp gives a tenant or group its own full copy of the stack, so load, failures and releases are contained to one stamp, and a stamp can be placed in a specific region for data residency. The price is running many copies of everything, which only works with strong automation.
6. Tiered pool + silo
In plain words: most tenants share a pool; the few who pay for (or legally need) isolation get their own silo, running the same code.
- Where the wall is: it depends on the tenant's tier, recorded in the catalogue.
- Pros: pool economics for the long tail and silo guarantees for the enterprise customers who fund them; isolation becomes a product tier you can price.
- Cons: two operating models to maintain and test; code must be tier-agnostic (no pool-only shortcuts); migrating a tenant between tiers is a real data-migration project.
- Who uses it: very common in B2B SaaS pricing — "dedicated instance" or "single-tenant" enterprise plans on top of a shared plan (ServiceNow-style dedicated instances are the far end of this). Atlassian keeps most Jira tenants on shared infrastructure and moves the very largest onto dedicated infrastructure.
- Sources: AWS SaaS Lens — tiering and mixed models · Azure — tenancy models, vertically partitioned deployments · AWS SaaS Architecture Fundamentals — full stack silo and pool · Azure SQL — hybrid sharded multitenant model
7. Tenant sharding (distributed database keyed by tenant)
In plain words: keep the pool model's shared tables, but spread tenants across many database nodes, keeping each tenant's rows together on one node.
- Where the wall is: still the tenant column (plus RLS if added); the sharding is about scale, not isolation.
- Pros: the pool model grows past one machine; queries scoped to one tenant stay on one node and stay fast; large tenants can be moved to their own node.
- Cons: cross-tenant queries fan out to every node; choosing
tenant_idas the distribution key must be done early (every table needs it, including in primary keys); rebalancing is an operational event. - When it is premature: it solves a data-size problem. Until one server (plus read replicas) cannot hold the pool, it only adds nodes to run.
- Who uses it: Citus (Postgres) was built around this for multi-tenant SaaS, and Freshworks runs Citus for billions of rows; Vitess shards MySQL similarly.
- Sources: Citus docs — multi-tenant applications · Vitess — sharding · Azure SQL — sharded multitenant databases
A: Sharding is about capacity, silo is about isolation. Tenant sharding keeps one logical database with shared tables and a tenant column, and distributes tenants across nodes so each tenant's rows are co-located — the application still relies on the tenant filter (or RLS) for separation, and many tenants share each node. Database-per-tenant gives every tenant a physically separate database, so separation holds even when a query forgets to filter. You can combine them, and systems often do: shard the pool, and silo the tenants who need it.
Comparing the models
Figures are orders of magnitude from vendor guidance, not benchmarks.
| Model | Wall enforced by | Cost per request | Noisy neighbour | Comfortable range | Grows by | Migrations per change | User spanning two tenants |
|---|---|---|---|---|---|---|---|
| Pool | application / ORM filter | ~0 with an indexed tenant column | shared | tens to 100k+ tenants | bigger server, read replicas, then sharding | 1 | easy |
| Pool + RLS | the database | small policy check per statement | shared | as pool | as pool | 1 | needs a membership-aware policy |
| Bridge | schema | ~0 | shared server | up to about a thousand schemas | catalogue bloat is the ceiling | N | union over schemas |
| Silo | database boundary | a connection pool per database | none at the data layer | up to about a thousand databases per pooled server | add database servers | N | cross-database query |
| Stamps | infrastructure | ~0 | none | one to tens of stamps | add stamps | N | effectively impossible |
| Tiered | per tier | as pool or silo | pooled tier only | many small + few large | move a tenant to a silo | 1 + k | mixed |
| Tenant sharding | tenant column, routed | ~0 when routed; cross-shard slower | per shard | 10k+ tenants, high volume | add worker nodes | 1 | cross-shard query |
Choosing a model
Answer these in order; each one eliminates models.
- How many tenants, and how big is the biggest? Thousands of small tenants rule out schema-per-tenant and full-stack silos on cost and operations.
- What isolation do customers or regulators demand? A contract that says "our data in a separate database" or a residency law settles it regardless of cost.
- Can one user belong to several tenants, or must you query across tenants? Silos make both expensive; pool makes them easy.
- What is the blast radius you can accept? If one tenant's load or one bad deploy must not affect others, you are heading toward stamps.
- Can you automate a fleet? Silo and stamps are only as good as your migration and deploy automation.
| If you have... | Prefer |
|---|---|
| up to hundreds of tenants on one database server | pool with an ORM filter, then RLS as the safety net |
| a few large or regulated tenants who need their own data | tiered: pool plus a silo for those few |
| tens of thousands of tenants or data past one server | tenant sharding, same code as the pool |
| a contract that demands separate hosting | a stamp for that customer only |
Refuse on the constraint first. If one user or integration must read two tenants in a single call, bridge, silo and stamps are out before cost is even compared. If you plan to consolidate services onto one database, the physically separated models fight that plan too.
A: A tiered model: keep the 2,000 small tenants in a shared pool — shared tables, a tenant column, an ORM-level filter, and Postgres row-level security as a second wall — and give the three enterprise customers their own databases running the same code, routed through a tenant catalogue that records each tenant's tier and location. That keeps the long tail cheap and turns isolation into something the enterprise plan pays for. The rules that make it work are that no code may assume the pool (every query is tenant-scoped either way), that migrations run through one pipeline for pool and silos, and that moving a tenant between tiers is a planned export-and-import, not an afterthought.
A: Because a single application-level filter is one bug away from a data breach, and cross-tenant leaks are among the most damaging failures a SaaS company can have. Adding an independent second mechanism — database row-level security that applies even when the query forgets — means two unrelated things must fail at once for a leak. The two walls also fail differently: the application filter is lost in a new endpoint, the RLS context is lost in connection handling, so testing each catches a different class of bug.
A: From something the caller cannot forge. The usual sources are a claim in a signed access token (a tenant_id claim issued by the identity provider), the host name (acme.example.com) mapped through the tenant catalogue, or an explicit selector such as a header that is checked server-side against the user's memberships — never a tenant id taken on trust from a request body or query string the user controls. Once resolved it is put into a request-scoped context that the data layer reads to set the ORM filter or the RLS session variable, so individual handlers never pass the tenant id by hand.
Resolving the active tenant
Once a user can belong to more than one tenant, every request must say which tenant it acts for, and the data layer needs that answer before its first query.
| Option | How | Strength | Weakness |
|---|---|---|---|
| Header | e.g. X-Tenant-Id, checked against cached memberships | no route changes; an integration can switch tenant per call | client must send it; a missing header is a 400 |
| Token claim | the tenant id is inside the signed token | nothing extra per call | switching tenant means a new token; multi-tenant clients juggle tokens |
| Path | /tenants/{id}/projects/3 | explicit, visible in logs | every route changes |
| Resource-derived | look up the owner of the requested resource | client sends nothing | list endpoints have no single resource to derive from |
| Host | acme.example.com | standard for SaaS portals | one call cannot span two tenants |
A header or path is only a selector: the server accepts it only if the authenticated user holds a membership in that tenant. Removing a membership should cut access as fast as removing any other permission (see token versioning in the Authorization models module).
Tenant wall plus grants: two layers
Tenancy and permissions answer different questions, and keeping them separate is what makes a forgotten check survivable.
| Layer | Answers | Built with |
|---|---|---|
| Tenant | which tenant's rows exist for this request at all | tenant column + ORM filter (+ RLS) |
| Membership | which tenants a user belongs to | a user-to-tenant membership table |
| Grant | which project or item inside the tenant, and view or edit | a grants table (user, role, scope) |
With the tenant wall in the data layer, a handler that forgets its permission check can still over-share inside one tenant, but it cannot leak another tenant's rows. Without it, every handler is one missing if away from a cross-tenant breach.
A: Because they fail differently and the tenant wall should not depend on every handler being correct. The tenant filter lives in the data layer and applies to every query automatically, so it decides which tenant's rows exist for the request at all. Permission checks decide which items inside that tenant a user may view or edit, and they live closer to the business logic where they are easier to forget. If a developer forgets a permission check, the damage stays inside one customer; if the tenant boundary were just another hand-written permission check, the same slip would expose another customer's data.
A: With an explicit selector the server validates, most often a header or a path segment, checked against the user's memberships before any query runs. Putting a single tenant in the token also works but forces a new token on every switch, which hurts integrations that act for several tenants in a row; deriving the tenant from the requested resource fails on list endpoints; a host name cannot span two tenants in one call. Whatever carries it, the resolved tenant goes into a request-scoped context the ORM filter and RLS read, and removing a membership must cut access as quickly as removing any other permission.