Background Mobile

Multi-Tenant SaaS: Where to Draw the Data Boundary

backend development/
September 17, 2026
Multi-Tenant SaaS: Where to Draw the Data Boundary

Where you draw the data boundary in a multi-tenant SaaS system is one of the most consequential architectural decisions you'll make. Get it wrong and you're either leaking data between tenants or paying ten times what you should for infrastructure. This post walks through the real trade-offs.

The Three Models and What They Actually Cost

Most multi-tenancy discussions collapse into a binary: shared database or separate database. The reality has a third option that sits between them, and the choice is less about security ideology and more about where your operational costs land.

Model Isolation level Infra cost per tenant Schema migration complexity Suitable tenant count
Shared schema, shared tables (row-level) Lowest ~$0 marginal Low 1,000+
Shared database, separate schemas Medium Low Medium 100–1,000
Separate database per tenant Highest High High <100

Row-level isolation means every table has a tenant_id column, and every query is filtered on it. At 10,000 tenants, this is the only model that doesn't bankrupt you. The risk is a missing WHERE tenant_id = ? clause somewhere in your codebase.

Schema-per-tenant is the middle ground. PostgreSQL handles this well with the search_path parameter. You get namespace separation without the overhead of spinning up a full database instance per tenant. Schema migrations get harder though — running ALTER TABLE across 500 schemas with zero downtime is a genuine operational challenge that most teams underestimate until they're in it.

Separate databases make sense when tenants have regulatory requirements that mandate physical data separation, or when they're large enough that their query load would affect other tenants on shared infrastructure. Below roughly 50 tenants it's also manageable operationally. Above that, you need automation for provisioning, migrations, monitoring, and connection pooling across N databases, and that automation is not trivial.

What Does "Data Boundary" Actually Mean in Practice?

It's worth being precise. A data boundary isn't just which rows a tenant can read. It covers:

  • Storage — where the data physically lives, and who shares that storage
  • Compute — whether one tenant's slow queries affect another's latency
  • Secrets and keys — are encryption keys shared or per-tenant
  • Audit logs — can one tenant's activity be distinguished from another's in your logging pipeline
  • Backups — can you restore a single tenant without touching others

Teams that focus only on query-level isolation often find they've missed the compute boundary. A single misbehaving tenant running an analytics query at 3 AM can saturate shared database CPU. PostgreSQL's pg_stat_activity will show you this happening. The fix is either separate read replicas, query timeouts enforced at the connection pool level (PgBouncer supports this), or tenant-level query quotas.

Encryption key isolation deserves its own mention. If you're using AES-256 with a single master key for all tenants, a breach exposes everything. Per-tenant keys managed through something like AWS KMS or HashiCorp Vault give you the ability to revoke one tenant's access without affecting others. The key management overhead is real but the blast radius reduction is significant.

/// Not sure where to start?

Get the architecture before you commit

Tell us what you're building and we'll map the technical approach, stack, and rough timeline. No cost, no obligation, no sales call required.

How Do You Enforce Isolation Without Making the Codebase a Mess?

This is where most teams have the actual problem. The data model choice is architectural. The enforcement is an engineering discipline problem.

Row-level security in PostgreSQL

PostgreSQL's row-level security (RLS), introduced in version 9.5, lets you define policies at the database layer rather than relying on application code. A policy like:

CREATE POLICY tenant_isolation ON orders
  USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

means that even if application code forgets a WHERE clause, the database rejects the cross-tenant read. This is a meaningful defence-in-depth layer. The cost is that you need to set the session variable on every connection checkout, which adds a small overhead per request — typically under 1 ms — but you need to account for it in your connection pool configuration.

Middleware enforcement

If you're not using database-level RLS, the next best option is a repository or data-access layer that wraps every query and injects the tenant context. The key is that this layer should be the only place in the codebase that constructs queries. Any ORM that allows raw SQL calls to bypass this layer is a liability.

Testing for isolation failures

Write tests that explicitly attempt cross-tenant reads. Create two tenant fixtures, authenticate as tenant A, and assert that tenant B's records are not accessible. This sounds obvious. Most codebases don't have these tests.

Should You Use a Connection Pool or Dedicated Connections per Tenant?

At scale, connection management becomes a real constraint. PostgreSQL supports a limited number of concurrent connections — typically around 100 to a few hundred before memory pressure becomes a problem. With thousands of tenants, you cannot have a dedicated connection per tenant.

PgBouncer in transaction-mode pooling is the standard answer. All tenants share a connection pool, but each transaction runs in isolation. The complication: PostgreSQL session-level settings (including search_path for schema-per-tenant models) don't persist across transaction-mode connections. You need to set context at the start of every transaction, not the session.

Some teams work around this with statement-level pooling and rely entirely on tenant_id columns. Others use Citus, which is an extension that adds distributed PostgreSQL capabilities and has native multi-tenancy support for sharding by tenant. If you're running on AWS, Aurora PostgreSQL with RLS handles the pooling problem differently by virtue of how its serverless scaling works, though you'll still want PgBouncer or RDS Proxy in front of it.

Compliance Boundaries Are Not the Same as Security Boundaries

SOC 2, ISO 27001, GDPR, and HIPAA all have opinions about data separation, but they don't all say the same thing.

GDPR doesn't require physical separation of EU and non-EU data, but it does require that you can identify, export, and delete a specific data subject's data on request. That's actually easier with row-level isolation than with separate databases, because your tooling is centralised.

HIPAA is stricter. A Business Associate Agreement (BAA) with your cloud provider doesn't automatically mean your tenants are isolated from each other. Your internal controls need to demonstrate that PHI from one covered entity cannot be accessed by another. That usually pushes towards schema separation at minimum.

If a tenant asks for a penetration test or audit and demands evidence of physical data isolation, "we use row-level security with PostgreSQL RLS" is a defensible answer. But some enterprise procurement teams won't accept it regardless of the technical merit. Know your market before you commit to a model.

Conclusion

Pick the isolation model that matches your tenant count, your compliance obligations, and your operational maturity — in that order. Row-level isolation with PostgreSQL RLS is the right default for most SaaS products. Move to schema-per-tenant when you have regulatory pressure or large tenants with noisy query patterns. Reserve database-per-tenant for situations where you have contractual obligations for physical separation or fewer than 50 tenants.

The concrete next step: audit your existing data-access layer and count how many places tenant context is passed as an application-level variable versus enforced at the database level. That number tells you your current blast radius.

FAQ

Is row-level security in PostgreSQL actually safe for multi-tenancy? Yes, when implemented correctly. PostgreSQL's RLS policies enforce tenant isolation at the database engine level, meaning a missing WHERE clause in application code won't leak data. The caveat is that superuser connections bypass RLS by default, so your application should never connect as a superuser.

When does schema-per-tenant become operationally unmanageable? Roughly around 500 to 1,000 schemas, depending on your migration tooling. Running zero-downtime ALTER TABLE migrations across hundreds of schemas simultaneously requires careful sequencing, good rollback plans, and automation. Teams underestimate this until they've shipped a migration that took four hours and half-completed.

Can you mix isolation models in the same product? Yes, and it's sometimes the right call. You can run SMB tenants on a shared schema and give enterprise tenants dedicated databases or schemas, with your application layer routing connections based on tenant tier. The added routing logic is manageable; the operational complexity of supporting two database topologies simultaneously is the actual cost.

Do separate databases per tenant solve the noisy-neighbour problem completely? Not entirely. You still share underlying infrastructure — network, disk I/O, and compute on the host — unless you're provisioning dedicated RDS or Cloud SQL instances per tenant. Dedicated instances solve the problem but cost scales linearly with tenant count, which is why this model only makes economic sense for a small number of high-value tenants.

How do you handle tenant offboarding cleanly? Row-level isolation makes deletion straightforward: DELETE FROM ... WHERE tenant_id = ? across your tables. Schema isolation makes it cleaner still: DROP SCHEMA tenant_xyz CASCADE. Database-per-tenant is cleanest of all but requires deprovisioning automation. Whichever model you use, test your offboarding path before you need it — GDPR right-to-erasure requests have a 30-day response window.

Have a project in mind? Contact Sodio Technologies to discuss your requirements and explore the right technology solution for your business.

/// Work with us

Talk to the engineers who'd build it

You'll get a technical scope, timeline and cost estimate from the people doing the work, not an account manager. In-house team, no subcontracting, since 2016.

Contact Us