"Multi-tenant SaaS on PostgreSQL: schema or row-level isolation"
Compare database-per-tenant, schema-per-tenant and shared tables with row-level security in PostgreSQL: isolation, cost, migrations, noisy neighbors, onboarding and reporting.

For most SaaS products, shared tables with a tenant ID and PostgreSQL row-level security (RLS) are the best starting point, because they are cheap to run and fast to onboard. Schema-per-tenant is a middle path when you need stronger separation and can live with heavier migrations. Database-per-tenant gives the hardest isolation and costs the most to operate, so it usually fits a small number of large or tightly regulated customers.
The right choice depends on how strong the isolation must be, how many tenants you expect, and how you plan to run migrations and reporting.
Why this choice matters
In a multi-tenant architecture, many customers share one running application. The database is where a mistake hurts most, because one missing filter can show one customer's data to another.
PostgreSQL supports all three models well, so the decision is about trade-offs, not capability.
The three models side by side
| Dimension | Database per tenant | Schema per tenant | Shared tables + RLS |
|---|---|---|---|
| Isolation | Strongest, physical | Good, via schema permissions | Logical, enforced by the database |
| Cost | Highest | Moderate | Lowest |
| Migrations | Once per database, partial failures | Once per schema, scripted loop | One change |
| Noisy neighbors | Best containment | Weak, shared instance | Weakest |
| Onboarding | Slowest, provision first | Fast, schema from template | Fastest, an insert |
| Cross-tenant reporting | Hardest, needs a warehouse | Workable with union views | Easiest |
Database per tenant
Each tenant gets its own database, and sometimes its own server. Credentials, backups and restores are separate, and a bug in one query cannot cross a database boundary. The cost is overhead per database, small tenants that waste capacity, and migrations you must run for every tenant while handling partial failures. PostgreSQL cannot join across databases in one query, so cross-tenant analytics needs an export pipeline.
Schema per tenant
One database, one schema per tenant, with identical tables in each. A wrong search path setting is the classic failure. Thousands of schemas mean thousands of copies of every table and index in the system catalog, which can slow tooling and backups, and long migrations multiply by tenant count.
Shared tables with row-level security
One set of tables with a tenant_id column on every row. RLS policies make the database itself filter rows by tenant. Once a policy is on, a query that forgets its WHERE tenant_id = ... clause still returns only that tenant's rows. The trade-off is performance isolation: one tenant's heavy query competes with everyone else's, and a very large tenant can skew table statistics.
Getting RLS right
RLS fails quietly if it is set up carelessly. A request should flow like this:
- 01Authenticateresolve the tenant
- 02Set contextper request or transaction
- 03Querypolicy filters rows
- 04Resetclear before pool return
- Enable RLS on every tenant-scoped table and write a policy that compares the row's tenant ID with a value from the current session.
- Set that session value per request or per transaction, and reset it when the connection returns to a pool.
- Connect the application as a role that does not own the tables and does not have the bypass attribute.
- Index
tenant_id, usually as the first column of composite indexes. - Write tests that try to read tenant B's data while authenticated as tenant A.
Treat RLS as a safety net in addition to application-level access control such as RBAC, not a replacement for it.
- RLS enabled on every tenant-scoped table
- Application role is not a table owner and cannot bypass policies
- Tenant setting reset when a connection returns to the pool
tenant_idindexed, usually first in composite indexes- A test proving tenant A cannot read tenant B
How to decide
Ask these questions in order:
- Do any customers require physical separation, their own encryption keys or a dedicated backup? If yes, give those customers their own database and keep everyone else on a shared model.
- How many tenants do you expect? A very large number of small tenants points to shared tables. A few dozen large ones make schemas or databases realistic.
- How uneven is tenant size? A few very large tenants alongside many small ones suggests a hybrid: shared tables by default, with a large tenant moved out when it needs it.
- How will you report across tenants? If you need cross-tenant analytics, favor shared tables or plan the warehouse pipeline early.
How this looks at Techdome
Our US and UK telehealth platform for the GLP-1 space is a multi-tenant clinical operating platform on PostgreSQL, serving 38 clinic tenants. It was built with HIPAA and UK GDPR architecture from day zero, so tenant separation sits alongside access control and auditing in the design rather than being added later.
For healthcare data, the isolation decision also feeds your compliance story, because reviewers and customers will ask how one clinic's records are kept from another. See healthcare software and HIPAA and our web and app development work for more on how we approach this.
Takeaways
- Shared tables with RLS give the best cost and speed for most SaaS products, with database-enforced filtering as a safety net.
- Schema-per-tenant suits moderate tenant counts but multiplies migration work.
- Database-per-tenant gives the hardest isolation at the highest operational cost, so reserve it for customers who need it.
- Choose based on isolation requirements, tenant count, size variance and reporting needs, and keep a hybrid path open.
- Test cross-tenant access explicitly. Do not assume the policy works.
Have a project like this? Talk to our engineers.


