Multi-tenant database architecture is a design for storing many customers in one data tier, isolated by row, schema, or separate database. Those three isolation levels are the entire decision space — everything else in this topic is consequences: how migrations run, what restore costs, where the noisy neighbor lives, and how far each pattern scales.
Key takeaways
| Question | Answer |
|---|---|
| The three patterns | Shared schema (tenant ID per row) · schema-per-tenant · database-per-tenant |
| Cloud vocabulary | The same three: pool · bridge · silo |
| Default choice | Shared schema, unless compliance or scale forces isolation |
| Practical ceilings | Silo: ~100s of tenants · bridge: ~1,000 · pool: millions (sharded: unlimited) |
| The iron rule | Enforce isolation in the database, not in every query |
The three patterns, in SQL
-- Pattern 1 · Shared schema ("pool"): one set of tables, tenant on every row
CREATE TABLE invoices (
tenant_id uuid NOT NULL,
id bigint GENERATED ALWAYS AS IDENTITY,
total numeric(10,2),
PRIMARY KEY (tenant_id, id) -- tenant leads: index- and shard-ready
);
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_rows ON invoices
USING (tenant_id = current_setting('app.tenant')::uuid);
-- Pattern 2 · Schema per tenant ("bridge"): one database, a namespace each
CREATE SCHEMA tenant_acme; -- same tables, repeated per tenant
-- Pattern 3 · Database per tenant ("silo"): full physical separation
CREATE DATABASE tenant_acme; -- strongest isolation; N of everything
On a managed backend the same guarantee is expressed without SQL: access control lives on the data itself, so isolation holds on every path — API, dashboard, or SDK:
// JavaScript / Node.js — Back4app JS SDK
// The same query for every tenant — ACLs scope results server-side
const query = new Parse.Query('Invoice');
const invoices = await query.find({ sessionToken: user.getSessionToken() });
// Only rows this tenant's role can read come back. No WHERE clause to forget. // Flutter / Dart — Back4app Flutter SDK
// The same query for every tenant — ACLs scope results server-side
final query = QueryBuilder<ParseObject>(ParseObject('Invoice'));
final response = await query.query();
// The session's role decides which rows exist, before results leave the server
if (response.success) {
print('${response.results?.length} invoices visible to this tenant');
} // iOS / Swift — Back4app Swift SDK
// The same query for every tenant — ACLs scope results server-side
let query = Invoice.query()
query.find { result in
if case .success(let invoices) = result {
print("\(invoices.count) invoices visible to this tenant")
}
} // Android / Kotlin — Back4app Android SDK
// The same query for every tenant — ACLs scope results server-side
val query = ParseQuery.getQuery<ParseObject>("Invoice")
query.findInBackground { invoices, e ->
if (e == null) {
Log.d("Billing", "${invoices.size} invoices visible to this tenant")
}
} Shared schema vs. schema-per-tenant vs. database-per-tenant
| Dimension | Shared schema | Schema-per-tenant | Database-per-tenant |
|---|---|---|---|
| Isolation | Logical, per row | Namespace | Physical |
| Tenant ceiling | Millions | ~Hundreds–1,000 | Tens–low hundreds |
| Cost per tenant | Lowest | Middle | Highest |
| Migrations | Run once, hit everyone | Run × N, orchestrated | Run × N, orchestrated |
| Per-tenant restore | Hard (selective copy) | Moderate | Trivial (restore one DB) |
| Noisy neighbor | Most exposed | Contained somewhat | Eliminated |
| Per-tenant customization | Hardest | Possible per schema | Easiest |
| Onboarding a tenant | Insert a row | Create a schema | Provision a database |
The details that bite
- Indexing is tenant-first.
tenant_idgoes on every table — even where joins make it look redundant — and leads every composite index:(tenant_id, created_at), not the reverse. Every real query is scoped to a tenant; the indexes should be too. - Migrations multiply with isolation. The silo’s flexibility costs an orchestration layer: version tracking per tenant, retry logic, drift detection. Teams underestimate this more than any other line in the table.
- Row-level security has operational fine print. Policies key off per-connection settings, which interact with connection pooling modes — set the tenant per transaction, and test the pooler. Also audit which roles bypass RLS; superuser paths are the classic hole.
- Connection arithmetic. A pool per tenant-database exhausts connections fast; shared-schema designs share one pool — another quiet advantage of the pool pattern that surfaces only at scale.
- Restore asymmetry drives tiering. “Can you restore just us to yesterday?” is an enterprise contract question; if the answer must be yes, that tenant belongs in a silo — which leads directly to the hybrid.
Scaling and the hybrid end-state
The growth path for the pool pattern is sharding by tenant: several shared-schema databases, each holding a slice of tenants, with a catalog mapping tenant → shard (open-source Citus built this into PostgreSQL, including moving a hot tenant to its own node). Combined with tiering, this yields the architecture most mature SaaS converges on: the free tier pooled, mid-size tenants pooled across shards, and the few regulated or enormous tenants siloed — with tenant_id kept in every schema everywhere, so any tenant can move between tiers without a remodel. Tenant mobility is the property to design for on day one; it is nearly impossible to retrofit.
Common use cases
- B2B SaaS. The defining case: every workspace, org, or team in your product is a tenant in one of these patterns.
- Freemium products at scale. Thousands of small free tenants pooled at near-zero marginal cost — the economics that make free tiers possible.
- Regulated verticals. Health, finance, and legal customers contractually requiring silos — served from the same codebase via the hybrid.
- Agencies and platforms. One application serving many client organizations, each with its own boundary.
- Internal multi-team platforms. Departments as tenants on shared tooling — same patterns, friendlier threat model.
Which pattern should you choose? A decision matrix
| Choose shared schema when… | Choose schema-per-tenant when… | Choose database-per-tenant when… |
|---|---|---|
| Tenants are many and small | Tenants number in the hundreds | Tenants are few and large |
| Cost per tenant must approach zero | Moderate isolation is worth some ops | Compliance demands physical separation |
| One migration should update everyone | Per-tenant schema tweaks are needed | Per-tenant restore is contractual |
| Self-serve signup, instant onboarding | Tenants onboard at human speed | Each tenant justifies provisioning |
| You’ll shard when you grow | You’ll cap tenant count | You’ll automate migrations × N |
And the meta-answer: pick per tier, not per company — hybrid designs put each tenant in the cheapest pattern that satisfies its requirements, and move them as requirements change.
Limitations and trade-offs
- Shared schema: the strongest need for database-enforced isolation — one missed filter is a breach, which is why RLS or data-layer ACLs are non-negotiable, not optional hardening.
- Schema-per-tenant: the awkward middle — migration orchestration like the silo, isolation weaker than the silo, and database metadata limits arriving surprisingly early.
- Database-per-tenant: everything × N — migrations, backups, monitoring, connections, cost — plus cross-tenant analytics becoming a data-engineering project.
- All three: tenant context invades everything (queries, caches, jobs, logs), and cross-pattern moves are expensive without day-one ID discipline: globally unique identifiers and
tenant_idcolumns everywhere, even in silos.
Multi-tenant data on Back4app
Back4app is an open-source Backend-as-a-Service (BaaS) platform that combines a managed database, auto-generated REST and GraphQL APIs, authentication, file storage, and Cloud Code serverless functions. Its answer to this article’s iron rule — enforce isolation in the database — is per-object ACLs, per-tenant roles, and class-level permissions, checked by the platform on every request from every surface, as shown in the code tabs above. That is the shared-schema pattern with the WHERE-clause risk removed; the engineering deep-dive on row-level security walks the full pattern on a document database, and the hosting-level view of the same topic covers the infrastructure tier above it.
Frequently asked questions
What are the three multi-tenant database patterns?
Shared schema — one set of tables, every row carrying a tenant identifier; schema-per-tenant — one database, a separate namespace of tables per customer; and database-per-tenant — full physical separation. Cloud architecture guides name the same three pool, bridge, and silo. Real systems increasingly mix them, tiering tenants across patterns by size and compliance needs.
Shared database or database-per-tenant — which should I choose?
The consensus default is shared schema unless something forces isolation: regulatory requirements, contractual data separation, heavy per-tenant customization, or single tenants big enough to need their own resources. Shared schema maximizes density and minimizes operations; per-tenant databases maximize isolation and multiply everything else — migrations, backups, connections, cost.
How many tenants can each pattern handle?
The practical ceilings from production experience: database-per-tenant runs comfortably to tens or low hundreds of tenants before operations dominate; schema-per-tenant reaches the hundreds to around a thousand before metadata and migration orchestration strain; shared schema scales to thousands or millions of tenants, and sharding the shared schema by tenant extends it effectively without limit.
How do you prevent one tenant seeing another tenant's data?
Defense in depth, never a WHERE clause alone. Application-level tenant scoping (middleware or ORM filters) is the first layer; database-enforced row-level security is the second — policies that filter every query by the current tenant regardless of what the application forgot. In shared-schema designs, a single missed filter is a cross-tenant breach, which is why the database itself should enforce the boundary.
How do schema migrations work across the patterns?
In shared schema, one migration updates every tenant at once — simple, with a blast radius to match. In schema- or database-per-tenant, the same migration must run once per tenant: hundreds of executions needing orchestration, version tracking, and drift detection, since a failed run leaves one tenant on an older schema. Migration tooling is the hidden tax of isolation.
Can you restore a single tenant's data?
In database-per-tenant, trivially — restore that database to any point in time, touching nobody else. In shared schema it is genuinely hard: the backup contains everyone, so you restore to a side instance and selectively copy the tenant's rows back. Per-tenant restore is one of the strongest practical arguments enterprises make for isolation tiers.
How should you index a shared-schema multi-tenant database?
Put the tenant identifier on every table — even where it looks redundant — and lead your composite indexes with it, so every query pattern becomes tenant-first. Making the tenant column the leading part of the primary key also pre-arranges the data for sharding by tenant later, which is the standard growth path.
What is sharding by tenant?
Splitting a shared-schema design across multiple databases, with all of one tenant's rows living on exactly one shard and a catalog mapping tenants to shards. It preserves shared-schema density while capping any single database's size, and enables moving hot tenants to quieter shards. The price is the catalog, rebalancing tooling, and the loss of trivial cross-tenant queries.