Skip to content
SaaS Development7 June 2026 · 8 min readUpdated 11 October 2026

Database Per Tenant vs Shared Schema vs Shared Database for SaaS

Database per tenant, schema per tenant or shared schema: how the three multi-tenant database designs compare on cost, migrations, connections and isolation, checked against primary docs.

Database Per Tenant vs Shared Schema vs Shared Database for SaaS

What you'll learn

  • Which multi-tenant database design should a SaaS start with?
  • When is database per tenant worth the cost?
  • What breaks first in a shared schema with RLS?
Article contents

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.

PatternIsolationMigrationsConnection pressureUsually fits
Shared database, shared schemaLogical, enforced by RLS or app filtersOne run for all tenantsLowestMost early and mid-stage B2B SaaS
Shared database, schema per tenantLogical, enforced by privilegesRuns once per schemaModerateSmall tenant counts with differing schemas
Database per tenantPhysicalRuns once per databaseHighestContractual 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.

An ornate bank vault door swung open to reveal a completely bare empty room with bare concrete walls, elaborate isolation architecture housing nothing inside. Risograph print in cobalt, hot pink, and mustard with halftone grain.

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.

Hundreds of identical metal filing cabinets packed floor-to-ceiling in a narrow corridor, drawers cascading open with papers spilling everywhere, the impossible operational weight of too many isolated containers. Risograph print in mustard and hot pink with halftone grain.

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.

A single elegant geometric block with multiple color-coded key-shaped cutouts on its surface, each silhouette glowing distinctly, one unified infrastructure, many locked compartments sharing the same foundation. Risograph print in cobalt and cyan with halftone grain.

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:

  1. Keep each policy to one indexed comparison on tenant_id, for example USING (tenant_id = (select auth.jwt() -> 'app_metadata' ->> 'tenant_id')::uuid). Supabase's RLS performance guidance recommends wrapping auth functions in select so the planner caches the result per statement, avoiding joins inside policies, and naming roles with TO.
  2. Index tenant_id as 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 in select. Those are Supabase's test numbers on their test data, so measure yours.
  3. Read tenant context from the JWT, and only from data users cannot edit. Supabase warns that a policy relying on user_metadata is unsafe because authenticated users can modify it, and to use raw_app_meta_data for authorization. Session settings are fragile too: Supabase's pooler docs say that in transaction mode "session-level state is lost between transactions", including set and reset.
  4. 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.

Free resource

Free SaaS MVP Scope Template

A Notion document with the full feature checklist, MVP vs. nice-to-have table, pre-build questions, and cost signals — so you walk into any developer call knowing exactly what to ask for.

Get the template →
DL

Dusko Licanin

Full-Stack Developer · Banja Luka, Bosnia

Full-stack developer shipping SaaS MVPs, web apps, and mobile apps using AI-augmented workflows — without agency coordination overhead. Live portfolio: BookBed, Callidus, Pizzeria Bestek.

Frequently Asked Questions

What is the difference between shared schema and database per tenant for SaaS?

Shared schema stores every tenant in the same tables, separated by a tenant_id column, while database per tenant gives each customer its own database. Shared schema has the lowest per-tenant cost in Microsoft's Azure SQL comparison and needs one migration run per release. Database per tenant gives physical isolation and per-tenant restores, but every schema change runs once per database and connection pools multiply. Its cost depends heavily on tooling: Microsoft rates it low per tenant when elastic pools are available, so the real price is the operational work on plain Postgres.

How should I approach multi-tenant database design for a new SaaS?

Start with a shared database and shared schema, keep tenant_id on every row and enforce isolation in the database as a backstop. This keeps migrations to one run and connection counts low, and it does not close any doors. Microsoft's hybrid model shows the exit: because every schema carries the tenant identifier, a tenant can later be moved to its own database and moved back. Write down your expected tenant count, your largest tenant and any contract that demands physically separate storage before you choose, since those three facts decide whether you ever need more than one database.

When does schema per tenant make sense in Postgres?

Schema per tenant makes sense only with a small, stable number of tenants that need different table structures. PlanetScale's 2026 analysis says schema-per-tenant likely won't scale beyond a few hundred tenants because catalogs grow into millions of rows and slow the query planner. The PostgreSQL documentation adds that schemas are not rigidly separated, so isolation comes from roles and USAGE privileges rather than search_path. Load-test with your real schema count before committing, because the threshold is a vendor estimate and depends on how many tables and indexes each tenant has.

How do I enforce tenant isolation in a shared Postgres database?

Use row-level security with one indexed comparison on tenant_id taken from a claim users cannot edit. Enable RLS on every table, add an index with tenant_id as the leading column, read the tenant from app_metadata instead of user_metadata, and wrap auth functions in a select so the planner caches them. Keep the service key out of request handlers, because it bypasses every policy. Treat the policies as a backstop behind application filters, which is also the approach PlanetScale prefers, and write a test that signs in as tenant A and tries to read tenant B.

Does Neon make database per tenant affordable?

Neon lowers the cost of reaching physical isolation but does not remove the operational work. Its documentation describes a branch as a copy-on-write clone created instantly regardless of database size, and idle databases scale to zero after 5 minutes of inactivity. You still run a migration for each database and still watch connection limits. Databricks announced an agreement to acquire Neon on 14 May 2025 without disclosing a price. Check current plan limits before modelling costs, since they change, and compare the result against a shared schema with row-level security.