Database-per-tenant vs shared schema: the decision is about migrations, not isolation
Shared schema, schema-per-tenant or database-per-tenant. Everyone frames it as isolation versus cost. The real fork is how many times you run every migration.
In short
Shared schema puts every tenant in the same tables behind a tenant_id column. Schema-per-tenant gives each its own namespace in one database. Database-per-tenant gives each its own instance. The usual framing is isolation versus cost. What actually decides it is how many times you will run every schema change.
Shared schema puts every tenant in the same tables behind a tenant_id column.
Schema-per-tenant gives each tenant its own namespace inside one database.
Database-per-tenant gives each its own instance.
Every post on this topic then draws the same table: isolation on one axis, cost on the other, pick your point on the line. That table is not wrong. It is just not the thing that decides the choice, because isolation is mostly solvable in software and cost is mostly the database line item, which is the smallest bill involved. The thing that decides it is a schema change.
Why the isolation framing misleads
The fear behind database-per-tenant is a query that forgets its WHERE tenant_id = ? and hands one customer another customer's rows. Real fear,
real incidents. But it is a fear about one bug class, and row-level security in
Postgres closes most of it at the database rather than in application code.
Once every table carries a policy that filters on the session's tenant, a
forgotten filter returns nothing rather than everything.
What row-level security does not solve is the tenant whose data is a hundred times bigger than anyone else's, sitting in the same table, slowing every query that touches it. That is the noisy neighbour, and it is a performance problem dressed up as an isolation problem. The shared schema handles it by extracting that one tenant, not by moving everyone.
So the isolation axis collapses to two honest cases. A contract or regulator requires physical separation, in which case the decision was made for you and the cost is simply what the contract costs to deliver. Or it does not, in which case row-level security plus the ability to extract a whale gets you almost all of the way there.
What a migration means in each model
This is the fork.
Shared schema. A migration runs once. It touches one set of tables. If it
fails, it fails once, at a known time, with one person watching. Adding a
column to projects is a Tuesday.
Database-per-tenant. The same migration runs once per tenant. With 100 tenants that is 100 runs. With 10,000 it is 10,000, and some of them will fail, because some tenant will have a row that violates the new constraint or a database that was mid-backup when the job arrived. Now someone is supervising a rollout, and rollouts across thousands of databases take days. The cost post on this pattern worked that supervision out at roughly $25,050 a month at 10,000 tenants, at a $150 an hour loaded rate. It is the largest line in that post by a wide margin. Larger than the databases.
Schema-per-tenant. The migration runs once per schema, which is once per tenant. So the migration cost is the database-per-tenant cost. And the connection pool, the backups and the noisy neighbour are the shared-schema problems, because it is still one instance. This is why the pattern reads well in a design doc and badly in an incident review.
Key takeaway
Count the migrations, not the databases. A shared schema runs each schema change once. Per-tenant models run it once per tenant, and past a few hundred tenants the supervision of those runs costs more than the instances do.
How to actually choose
Two numbers decide it: how many tenants, and how much each one pays.
| Tenants | Contract size | Model |
|---|---|---|
| A handful | Six figures, with data terms in the contract | Database-per-tenant |
| Hundreds to thousands | Self-serve, per seat or per usage | Shared schema, row-level security |
| A shared base plus one or two whales | Mixed | Shared schema, extract the whales |
| Any | Any | Not schema-per-tenant, unless the count is small and will stay small |
Five customers paying six figures each, with a clause about where their data lives, is a per-tenant product whatever the ops cost, because the ops cost is in the price. Five hundred teams paying per seat is a shared schema, because the alternative is a migration rollout that competes with the roadmap every sprint.
Under about 100 tenants, none of this adds up to much either way, which is exactly when it is hardest to migrate away from whatever you picked. Pick for the count you expect at the point the product is working, not the count you have now.
When each one is the wrong choice
Database-per-tenant is wrong for a self-serve product. A signup that provisions a database is a signup that takes seconds instead of milliseconds, costs money whether or not the customer ever returns, and adds one more migration target for every trial that churns. The pattern assumes tenants are few and valuable. Free tiers violate both assumptions at once.
Shared schema is wrong when the contract says otherwise. Not when the customer prefers otherwise, or when a security questionnaire has a checkbox for it. When the signed terms require it. Everything short of that is a conversation about row-level security and a data-residency region.
Schema-per-tenant is wrong more often than it is right. It is the pattern teams reach for to avoid choosing, and it postpones the choice by inheriting both sets of problems. If the count is genuinely small, say a dozen tenants that will stay a dozen, it is fine. If the count is going to grow, it is the one you will regret.
What this looks like from where we sit
BuildBase runs both, at two different levels, and that turns out to be the useful way to think about it. Each customer of ours, an org building a product on the SDK, gets its own database. Inside that, the org's own customers are workspaces, which share the org's schema and are isolated by workspace rather than by instance. A schema change on the workspace model runs once per org database, and the ceiling an org hits is workspace count per plan rather than a connection limit per workspace.
Which is to say: per-tenant where tenants are few and paying, shared where they are many and self-serve, and the boundary drawn at the point where the tenant count changes character. That is the same rule as the table above, applied twice. The workspaces guide is the working-code version of the shared half, and the cost post is the arithmetic behind the per-tenant half.