Dawloom
All posts

Multi-tenant SaaS: row, schema, or database isolation

Dawloom engineering5 min read

Every multi-tenant SaaS product has to answer one structural question before the first migration ever runs: where does one tenant’s data physically stop and another’s start? There are three common answers, usually called row, schema, and database isolation. Each one trades operational simplicity for blast-radius containment in a different place, and the right answer depends more on who your tenants are than on which pattern looks cleanest in a diagram.

Row-level isolation

The cheapest model to run is one schema, one set of tables, and an organization id column on every row that matters. Every query filters by that id, either in application code or, if the database enforces it directly, through PostgreSQL’s row security policies. You enable it per table and attach a policy:

alter table quotes enable row level security;

create policy org_isolation on quotes
  using (organization_id = current_org_id());

Once enabled, Postgres applies a default-deny rule: rows are invisible unless a policy explicitly allows them, on every query, not just the ones a developer remembered to scope. Table owners and superusers bypass row security by default, which matters if your migration tooling connects as the table owner.

Row isolation keeps ops simple. One database to back up, one set of migrations to run, one connection pool to size. It also means every tenant shares the same physical tables, so a bug in a WHERE clause, or a role with the bypass attribute, can leak rows across tenants. FurnitureAxis, our multi-tenant furniture-retail SaaS built on Next.js, isolates every record by organization id at this level. Warehouse data, quotes, and purchase orders for one retailer never show up in another retailer’s queries, because every table that holds tenant data carries that id and every read is scoped to it.

Schema-per-tenant isolation

The middle option keeps one database but gives each tenant its own Postgres schema, so acme.orders and beta.orders are separate tables that happen to share a server. This narrows the blast radius of an unscoped query, since there’s no shared orders table to query by mistake, without paying for a fully separate database per tenant.

The cost shows up in migrations and connection management. Adding a column now means running that migration against every tenant’s schema, not once. Connection pooling and the schema search path both need to track which tenant a connection is currently serving, which is more moving parts than a single id column. For a product with a handful of large tenants, that overhead is manageable. For a product onboarding new tenants often and automatically, it turns into a queue of schema-creation and migration jobs that has to keep pace with signups.

Database-per-tenant isolation

The strongest isolation gives every tenant an entire database. A backup restores one tenant without touching any other. A noisy tenant’s queries can’t starve another tenant’s connection pool. Compliance requirements that specify physical data separation, which come up more in regulated and larger enterprise contracts, are easiest to satisfy here because there’s nothing to argue about: the data sits on a different database, sometimes a different server.

The bill for that isolation is operational. Migrations run once per tenant database, which turns a five-minute schema change into a fleet-wide rollout with its own failure modes. Connection pools multiply. Reporting across tenants, if the business ever needs it, means querying N databases and merging the results instead of running one query with a WHERE clause. This model tends to earn its cost only once a tenant is large or regulated enough that paying for isolation per customer makes sense.

Three panels showing row, schema, and database isolation on PostgreSQL, from one shared table to separate databases per tenant

What we weigh when a client asks

We don’t default to one model. The questions that actually move the decision are about the tenants, not the technology.

How many tenants are there, and how big is each one? A product with thousands of small tenants usually can’t afford a database each. The cost per tenant has to stay low, which points toward row isolation with careful query scoping. A product with a few dozen large enterprise tenants can afford heavier isolation per customer, and those customers are more likely to ask for it by name.

Who’s asking for isolation, and why? “We want strong tenant isolation” sometimes means a real requirement, data residency or a specific audit standard, and sometimes means a general unease that a shared table sounds risky. The first case is worth solving with schema or database isolation. The second is often better solved with row security policies enforced at the database layer, which give a real, auditable guarantee without the cost of running N databases.

How often does the schema change? Row and schema isolation both mean N migration targets once you move from one shared table to per-tenant structures. If your product is still finding its data model, that cost compounds every sprint. A product with a settled schema absorbs it better.

This is the same category of question that shows up on ERP and operations builds, where per-client data separation, audit trails, and cross-tenant reporting all pull on the isolation decision at once. It’s a decision worth having before the first migration runs, not after a customer asks where their data lives and the honest answer turns out to be “the same table as everyone else’s.”

The verdict

Start with row-level isolation enforced by Postgres row security policies, not application-code filtering alone, unless you already know a customer needs harder guarantees. It’s the cheapest to run, and a database-enforced policy closes the gap between the app being supposed to filter by tenant and the database refusing to return the wrong tenant’s rows even if the app forgets. Move to schema or database isolation only when a specific tenant or a specific compliance requirement demands it, because that’s the point where the isolation is worth what it costs to run. If you’re scoping a multi-tenant build and need to work out which side of that line your product sits on, tell us what you’re building and we’ll help you size it against your actual tenant count, not a hypothetical one.

Got something to build?

Tell us what you need. An engineer replies, not a sales team.

Search the whole site