nestonexStart a project
NESTONEX PLANNING NOTES

Multi-tenant SaaS: shared database or a database per tenant?

Every multi-tenant product makes this decision once and lives with it for years. The right answer depends on isolation requirements, tenant count and how different your largest customer is from your smallest — not on which approach sounds more professional.

Start with a shared database and row-level isolation enforced in the database rather than in every query. Move a tenant to its own database when contractual isolation, independent restore, or noisy-neighbour effects justify the operational cost. Most products need shared; a few large customers need separate; almost nobody needs separate for everyone from day one.

The three shapes

Shared database, shared schema. Every tenant's rows live in the same tables, distinguished by a tenant column. Cheapest to operate, easiest to migrate, and the model where a missing filter is a disclosure.

Shared database, schema per tenant. One database, one schema each. Better logical separation, and migrations now run once per schema, which stops being funny somewhere around the fiftieth tenant.

Database per tenant. Full isolation, independent backup and restore, and the ability to host a customer in their own region. Also the highest fixed cost per tenant in connections, migrations, monitoring and deployment coordination.

Most products should start at the first and keep the third available for the customers who need it, rather than choosing once for everyone.

  • Shared schema: cheapest, most migration-friendly, needs enforced isolation
  • Schema per tenant: better separation, migration cost scales with tenants
  • Database per tenant: strongest isolation, highest fixed operational cost

Making shared genuinely safe

In a shared schema, every query needs a tenant filter, and there will be hundreds of queries written by people who joined at different times. It works until one does not have it — a new endpoint, a reporting query, a migration script, a join reaching a table through a path nobody checked.

The asymmetry is what makes this worth solving structurally: hundreds of chances to get it right, one chance to get it wrong, and the wrong outcome is a disclosure rather than a defect.

Row-level security moves the filter into the database, attached to the table and tied to a session variable your application sets. A query that forgets its clause returns zero rows instead of somebody else's data. The bug still exists; it now presents as an empty page rather than an incident.

Keep the application filters as well. They express intent and help the planner. The policy is the thing that holds when one is missing.

When a tenant earns its own database

Contractual isolation, where a customer's agreement or their regulator requires their data to be physically separate and a policy on a shared table will not satisfy an auditor.

Independent restore. Recovering one tenant to a point in time from a shared table is a painful bespoke operation, and you do not want to be designing it during the incident that requires it.

Noisy neighbours. One tenant whose volume degrades everyone else's queries through shared indexes and shared buffers. This is real, and it usually arrives as a support ticket from a different customer.

Data residency. A customer who must be hosted in a specific region, which is a hosting decision a shared database cannot express.

What separate actually costs

Migrations multiply. A schema change now runs N times, with partial-failure states in between, and needs tooling that tracks which tenants are on which version.

Connections multiply. Connection pools are per database, and a few hundred tenants will exhaust what your database can hold long before your traffic does.

Observability fragments. Cross-tenant analytics — the numbers your own business runs on — now require either a warehouse or a query fan-out, and that is a system somebody has to build.

And deployment coordination becomes a real activity rather than a step. None of this is prohibitive at ten tenants. All of it is at a thousand.

How this looks in practice

AgencyOS runs operator, admin and client portals off one API across 78 data models, with each operator's marketing site provisioned from an embedded generator. At that surface area, isolation cannot depend on every query author remembering a clause — it has to be enforced somewhere it cannot be forgotten.

ProTrader is multi-tenant with white-label provisioning, where each community's members and computed broker metrics must never cross. Same reasoning: shared code paths, per-tenant data, enforcement at the layer that does not rely on discipline.

The migration path matters as much as the starting point. Building shared-first with a tenant id on every row and isolation enforced in the database leaves the door open to extract a large customer later. Starting with a database per tenant and trying to consolidate is considerably harder.

COMMON QUESTIONS /

Questions people also ask

Is a shared database less secure?

Not inherently — it is less forgiving. Separate databases fail safe when a query is wrong; a shared one leaks unless isolation is enforced below the query. With row-level security the failure mode becomes an empty result, which is the behaviour you want from a mistake.

How many tenants before a shared database stops working?

Tenant count matters less than tenant variance. A thousand similar small tenants are comfortable in a shared database. Ten tenants where one is fifty times the size of the others will cause noisy-neighbour problems regardless of the total.

Can you move one tenant to a separate database later?

Yes, if every row carries a tenant id and the application does not assume cross-tenant joins. That is the main argument for building shared-first with strict isolation: it keeps the option. Consolidating separate databases back into one is much harder.

What about a separate schema per tenant?

It is a reasonable middle ground for tens of tenants, giving better logical separation than a shared schema without the connection overhead of separate databases. Its cost is migration: every change runs once per schema, and that arithmetic turns against you as you grow.

THE PROOF /

Where we have actually done this.