Journal
5 min readMulti-Tenant SaaS: The Decisions That Actually Matter
Why row-level security beats application-layer filtering, when the isolation model becomes expensive to change, and what else shifts once a SaaS serves many customers.
- multi-tenant SaaS
- Supabase multi-tenant
- row level security
- RLS Postgres
Most SaaS products become multi-tenant by accident. A single-customer build picks
up a second customer, someone adds an account_id column, and the isolation
model is whatever the first ten queries happened to do. That works until it
doesn't, and when it stops working it stops working as a data leak.
Multi-tenancy is not a feature you add. It is a constraint you either design around or retrofit under pressure.
What does multi-tenant actually mean?
One running instance of your application serves many customers, and no customer can see another's data. That second clause is the whole problem. The first is just deployment.
Three patterns dominate, and they trade isolation against operational cost:
| Pattern | Isolation | Cost to operate | Fits |
|---|---|---|---|
| Database per tenant | Strongest | Highest — migrations multiply | Regulated, few large tenants |
| Schema per tenant | Strong | Moderate | Mid-size B2B, tens of tenants |
| Shared tables, row-level policy | Weakest by default | Lowest | Product-led SaaS, many tenants |
The third row is where most products land, and it is the one that punishes carelessness. Isolation stops being a property of your schema and becomes a property of every query you write.
Why row-level security instead of application-layer filtering?
The common approach is to filter in the application: every query carries a
where org_id = ? and code review catches the ones that forget. This works
until someone writes a report, a background job, an admin tool, or an export
script — and one of those forgets.
Row-level security moves the rule into the database. The policy is declared once against the table, and it applies to every query that touches it regardless of which service, job, or console issued it. An application bug becomes an empty result set instead of another tenant's data.
On PeoplePixel, a lead-attribution platform we built for Click Track Marketing, the data model was multi-tenant with RLS from the start rather than added later. The product ingests crawl data, SEO data, CRM contacts and visitor identification into one system, and pipeline automation runs on cron jobs and webhooks — code paths that execute with no user in the request context. Those are exactly the paths where application-layer filtering fails quietly.
When does the isolation model become expensive to change?
At the point you have real customer data in production, which is earlier than most teams expect.
Retrofitting isolation means backfilling a tenant key across every table, auditing every existing query, and doing it without downtime on data you cannot afford to mix. The work is not conceptually hard. It is just slow, high-risk, and impossible to parallelise, which is a bad combination when it lands in the same quarter as your first enterprise deal.
The cheap version of this decision costs a day at the start of a project. The expensive version costs a sprint and a security review.
What else changes once you are multi-tenant?
Isolation gets the attention, but it is not the only thing that shifts:
- Migrations now run against data shaped by many customers, not one. A migration that assumes a column is populated will find the tenant where it isn't.
- Background jobs run outside a user session. They need an explicit tenant context, and they are the most common source of cross-tenant bugs.
- Admin tooling has to deliberately cross the boundary, which means it needs its own access path and its own audit trail.
- Per-tenant configuration multiplies. On Atlex, a legal deadline-tracking platform, we handled this with reusable logic templates: deadline and due-date rules composed once, then applied across case types. The alternative — rules hard-coded per customer — is the version that becomes unmaintainable at customer eight.
How do you decide, in practice?
Ask two questions before writing any schema.
How many tenants, realistically? Ten enterprise customers and ten thousand self-serve accounts are different products with different correct answers. Database-per-tenant is reasonable at ten and absurd at ten thousand.
What happens if two tenants' data mixes? For an analytics dashboard, it is an embarrassing bug. For legal case deadlines, healthcare records, or financial attribution, it is an incident with legal consequences. The cost of the worst case sets the floor on how much isolation you buy.
Most products should start with shared tables and row-level policies, declare the policies before the first feature, and stay honest about the fact that convenience methods which bypass them are the thing that will eventually hurt.
This is a working note from building multi-tenant products, not a specification. The full engineering detail lives in the case studies: PeoplePixel and Atlex.