You are about to pick how a SaaS stores its customers' data, and someone on the team has said the word "isolation" in a worried voice. Before you provision anything, write down three numbers: how many tenants you expect in 12 months, how big the largest one could get, and whether any contract demands a physically separate database. Those three answers decide most of this post.
Database per tenant means every customer gets its own database. Shared schema means every customer lives in the same tables, separated by a tenant_id column. Schema per tenant sits between them. For a typical early-stage B2B product, shared schema with database-enforced isolation is the cheapest place to start, and database per tenant is worth paying for only under specific conditions. The rest of this post shows which conditions, using multi-tenant SaaS sources I checked on 30 September 2026.
Which multi-tenant database design should a SaaS start with?
For most SaaS products, a shared database with a shared schema is the right start: it has the lowest per-tenant cost and one migration path, and you add isolation rules in the database instead of extra infrastructure.
| Pattern | Isolation | Migrations | Connection pressure | Usually fits |
|---|---|---|---|---|
| Shared database, shared schema | Logical, enforced by RLS or app filters | One run for all tenants | Lowest | Most early and mid-stage B2B SaaS |
| Shared database, schema per tenant | Logical, enforced by privileges | Runs once per schema | Moderate | Small tenant counts with differing schemas |
| Database per tenant | Physical | Runs once per database | Highest | Contractual separation, one very large tenant |
Two sources give the trade-offs in numbers. Microsoft's Azure SQL guidance, last updated in 2026, rates a multitenant database as the lowest per-tenant cost and notes it "necessarily sacrifices tenant isolation", with a higher risk of noisy neighbors. PlanetScale's April 2026 write-up on tenancy in Postgres says schema-per-tenant and database-per-tenant "likely won't scale beyond a few hundred tenants", while shared schema "can easily scale to many thousands of tenants". That is a vendor's view, but it matches the connection and catalog mechanics described below.
Why does database per tenant get expensive to run?
The cost is operational before it is financial. Each extra database is another backup schedule, another migration target, another connection pool and another dashboard.
My take is that database per tenant is usually fear dressed up as architecture: isolation you will never need, plus an ops burden you cannot staff.
I should correct a number you may have seen repeated: that database per tenant is always the most expensive model. Microsoft's own comparison table rates database-per-tenant per-tenant cost as "Low; pools used", because Azure SQL elastic pools share resources across databases. What Microsoft calls the most expensive model is the standalone app per tenant, where pools cannot be used. So the cost depends on tooling. Azure SQL has management features built for "well over 100,000 databases", and Microsoft's example is telling: a 1,000-tenant database with 20 indexes becomes 20,000 indexes once you split it into 1,000 databases. Automatic tuning handles that on Azure SQL. On a plain Postgres instance you would be writing the orchestration yourself.
Connections are the harder limit on Postgres. PlanetScale points out that PgBouncer pools are calculated per database, so they "will quickly exceed your max_connections limit". Even a pool of a few idle connections per tenant adds up fast across hundreds of databases.
Migrations are the third cost. Consider a two-person team adding a soft-delete column. On a shared schema that is one migration file and one run. With 50 tenant databases it is 50 runs, a script to loop them, and a retry path for any database that was unreachable. That is a hypothetical, but the arithmetic is the same for every schema change you ship afterward.
Is schema per tenant a safe middle option?
Schema per tenant is the pattern I would avoid, because it carries a fleet's migration cost without a database's isolation.
The Postgres catalogs are the first problem. PlanetScale states that with hundreds of schemas, each holding tables and indexes, "these catalogs grow into millions of rows", which slows the query planner because it consults the catalog on every query. That is their analysis, so treat the exact threshold as an order of magnitude and load-test your own schema count before committing.
The second problem is what a schema actually guarantees. The PostgreSQL documentation on schemas says that "schemas are not rigidly separated: a user can access objects in any of the schemas in the database they are connected to, if they have privileges to do so." The search_path setting only decides which schema an unqualified table name resolves to. Real separation comes from roles and USAGE privileges, which means one role per tenant and a connection layer that switches between them. If your application connects with a single role, a wrong search_path points a query at the wrong tenant's tables, and nothing in the database objects.
When is database per tenant worth the cost?
It is worth paying for in three cases: a contract that demands physical separation, one tenant that consumes most of the resources, or per-tenant schema customization that shared tables cannot hold.
In each case the answer is usually a hybrid, not a full switch. Microsoft's hybrid model keeps the tenant identifier in every schema, so any tenant can move to its own database later and move back. Their example moves free-trial tenants to a shared database and gives a premium subscriber a single-tenant database. Microsoft also warns that switching models later is "sometimes costly", which is a reason to keep tenant_id on every row even if you plan separate databases for a few customers.
Here is the decision that shaped the Callidus case study, in my own words from the project:
Callidus is the exact case people invoke for database-per-tenant: UK aesthetic clinics, patient records, GDPR, real regulatory isolation pressure. I went shared Firestore instead — tenants/{tenantId}/... paths, JWT-scoped role claims, and the mutation guard enforced at both the client routes and the Firestore rules. Three years earlier I would have reflexively spun up a database per clinic and burned half my prod ops budget on it. The decision that saved me was not a clever pattern; it was admitting clinics never actually look at each other's data, so the isolation guarantee just needs to be enforceable, not physical.
Callidus runs on React, Firebase and Stripe Connect, so there is no Postgres in that story. The lesson that transfers is the question, not the stack: does the isolation need to be physical, or only enforceable? I compare the two enforcement styles in Firestore rules vs Postgres RLS.
Does Neon's branching make database per tenant affordable?
It lowers the barrier, but it does not remove the operational work. Neon's documentation describes a branch as "a copy-on-write clone of your data", created instantly regardless of database size, and its scale-to-zero feature suspends an idle database after 5 minutes of inactivity and reactivates it within a few hundred milliseconds (paid plans can turn this off). Databricks announced its agreement to acquire Neon on 14 May 2025; the release does not disclose a price. A database per tenant on that model costs little while idle. You still run one migration per database and still need to watch connections, so I would call it a cheaper way to reach physical isolation and not a way to skip the ops work. Check the current plan limits on Neon before you model costs, since those change.
What breaks first in a shared schema with RLS?
The first failures are slow policies and leaked privileges, not the isolation idea itself. A shared schema with Postgres row-level security stays fast and safe if you follow four rules.
First, know what RLS does and does not cover. The PostgreSQL row security documentation says a table with RLS enabled and no policy is default-deny, and that superusers and roles with BYPASSRLS always bypass it. Table owners bypass it too unless you run ALTER TABLE ... FORCE ROW LEVEL SECURITY. PlanetScale says it does not recommend relying on RLS and prefers application-layer enforcement. I treat database policies as the backstop behind application filters, not a replacement for them. That is a recommendation, and teams differ.
Follow these four rules for the policies themselves:
- Keep each policy to one indexed comparison on
tenant_id, for exampleUSING (tenant_id = (select auth.jwt() -> 'app_metadata' ->> 'tenant_id')::uuid). Supabase's RLS performance guidance recommends wrapping auth functions inselectso the planner caches the result per statement, avoiding joins inside policies, and naming roles withTO. - Index
tenant_idas the leading column. Supabase's own benchmark table shows a test query dropping from 171 ms to under 0.1 ms after an index was added, and from 178,000 ms to 12 ms after wrapping a function inselect. Those are Supabase's test numbers on their test data, so measure yours. - Read tenant context from the JWT, and only from data users cannot edit. Supabase warns that a policy relying on
user_metadatais unsafe because authenticated users can modify it, and to useraw_app_meta_datafor authorization. Session settings are fragile too: Supabase's pooler docs say that in transaction mode "session-level state is lost between transactions", includingsetandreset. - Keep the service key out of request paths. Supabase states that "a secret key bypasses every Row Level Security policy you have", so admin jobs, webhook handlers and cron tasks belong in separate backend code, never in a browser and never in a handler that accepts a tenant id from the client. The React + Supabase RLS stack notes show one way to split that code.
If you want the full shared-schema build order, the multi-tenant SaaS architecture guide and the Supabase RLS walkthrough cover it, and the multi-tenant tooling shortlist lists what to add around it.
Which of your tenants would actually refuse a shared schema in writing: 30 enterprise accounts with isolation clauses, or 3,000 small ones who care about price and speed? Name the first tenant whose contract says "separate database" and check whether the clause requires physical separation or only enforced separation.



