DEV Community

Suraj Upadhyay
Suraj Upadhyay

Posted on Originally published at esence.io

PostgreSQL Multi-Tenancy: Isolation That Survives a Growing Team

Startups building B2B products reach for multi-tenancy in PostgreSQL the same way on day one: one shared database, one set of tables, and a tenant_id column marking who owns each row. That is the correct call, and it stays correct for a long time. However, when that column is enforced by application code rather than by the database, a single forgotten predicate stops being a bug and becomes a disclosure event, and a disclosure event is one of the very few engineering failures that lands straight on your balance sheet as stalled enterprise deals, an unplanned legal bill, and a security review you can no longer pass. By understanding what multi-tenancy actually guarantees, which isolation model fits your stage, and how Row-Level Security moves that guarantee out of your codebase, startup CTOs and Fractional CTOs can make the tenant boundary hold without slowing the team down.

(If you want to skip the theory, jump straight to the connection pooler trap that switches Row-Level Security off in production, what it costs in query performance, or when it is genuinely time to leave the shared schema.)

Because "enforced by application code" means something very specific in practice. It means a promise that everyone will remember to filter on tenant_id, and that promise is the single most expensive line of undocumented policy in your entire codebase, because it holds perfectly for about fourteen months, right up until the afternoon a tired engineer ships a reporting endpoint that joins four tables and forgets the predicate on exactly one of them, and then a customer opens a dashboard and sees somebody else's invoices.

That is not a bug. A bug is something you fix on Monday. A cross-tenant data leak is a disclosure event, which means legal gets involved, your enterprise prospects get an email from their own security team, and the deal that was supposed to close your Series A quietly moves to next quarter and then to never.

The uncomfortable part is that this is not a story about careless engineers. It is a story about an architecture that requires every engineer to be careful forever, which is not an architecture at all, it's a hope.

What is multi-tenancy in PostgreSQL?

Multi-tenancy in PostgreSQL is the practice of serving multiple customers, called tenants, from a single database system while guaranteeing that no tenant can read or modify another tenant's data. PostgreSQL supports this at three levels of physical separation: a shared schema where a tenant_id column marks ownership of each row, a separate schema per tenant, or a separate database per tenant.

The word doing all the work in that definition is guaranteeing. Any database can store several customers' rows in one table. What distinguishes a real multi-tenant architecture is where the guarantee lives, either in application code that every engineer must remember to write, or in the database itself, where it holds whether or not anyone remembered.

PostgreSQL has shipped a mechanism for the second option since version 9.5, in 2016. It is called Row-Level Security, and most of this post is about using it without stepping on the three mines buried around it.

The three isolation models, and what each one really costs

There are exactly three shapes here, and every vendor blog that tells you otherwise is selling something.

Shared schema + tenant_id Schema per tenant Database per tenant
Separation Logical, by column Logical, by namespace Physical
Cost per tenant Near zero Catalog rows, real at scale An instance
Migrations One run A loop over tenants A loop over instances
Cross-tenant analytics A query A fan-out job A pipeline
Noisy-neighbour control None None Full
Restore one tenant Hard Moderate Trivial
Enforcement RLS, or app code Namespace + search_path Connection string
Choose this when Default. Almost everybody. Tenant schemas genuinely diverge A named customer or a residency law demands it

Most startups pick the shared schema correctly and then defend it incorrectly, which is a distinction worth sitting with, because the shared schema really is the right default for almost everybody reading this. You should not be running a schema per tenant at eleven customers, and the founder who spent a quarter building a database-per-tenant provisioning pipeline before finding product-market fit has bought a very good insurance policy on a house he has not finished building.

The mistake is not picking the shared schema. The mistake is enforcing it in application code.

How Row-Level Security actually works

Row-Level Security is a PostgreSQL feature that attaches a boolean policy to a table, which the planner then welds onto every query touching that table, so that rows failing the policy are invisible regardless of what the query asked for. A missing WHERE tenant_id = ... stops being a data leak and starts being an empty result set, which is a category of failure your QA process can actually
catch.

To read the full articles you can visit 👉 PostgreSQL Multi-Tenancy: Isolation That Survives a Growing Team


I write about startup systems architecture at
esence.io. If your tenant boundary is currently held together by code review, that is the kind of thing I look at for a living.

Top comments (0)