A multi-tenant SaaS is one application serving many isolated customers (tenants). One codebase. One database. Many companies whose data must never accidentally bleed into each other. The architectural decisions you make in week one determine how cleanly the system can grow from 1 tenant to 10,000.
The three approaches — pick one, and pick on purpose
- Schema-per-tenant. Every tenant gets its own database schema. Strong isolation. Easy backups per tenant. Painful for cross-tenant analytics. Best for enterprise SaaS with strict data residency or compliance needs.
- Database-per-tenant. Even stronger isolation than schema-per-tenant. Each tenant gets a separate database. Excellent for regulated industries. Operations cost scales linearly with tenant count — fine for 50 tenants, painful at 5,000.
- Shared schema with row-level isolation. All tenants share the same tables; every row has a
tenant_id; Postgres row-level security (RLS) enforces the filter. Cheapest to operate, most flexible to query, scales to thousands of tenants. The right default for most SaaS.
We pick shared-schema-with-RLS for ~80% of new SaaS builds. Schema-per-tenant for enterprise with hard isolation requirements. Database-per-tenant rarely — usually for fintech or healthtech where regulators expect it.
Why row-level security is the multi-tenant superpower
Without RLS, every query in your application must filter by tenant_id. Forget one filter, leak another tenant's data. It is a tax on every line of code and a risk on every code review.
With Postgres RLS, the database itself enforces the filter. Even if your application code forgets the WHERE clause, the query will return zero rows for the wrong tenant. It is defence in depth, free, built into Postgres, and the single most important technical decision you can make on day one of a multi-tenant build.
How RLS actually works
On every connection, you set a session variable: SET LOCAL app.current_tenant = '...'. Every table that holds tenant data has an RLS policy: USING (tenant_id = current_setting('app.current_tenant')::uuid). The database transparently filters every query by that variable. Your application code reads and writes as if it were a single-tenant system; the database makes sure the right rows come back.
What to get right on day one
- tenant_id on every tenant-scoped table. Not nullable. Foreign key to a
tenantstable. Indexed. - RLS policies enabled and tested. Write integration tests that try to access data from the wrong tenant and assert that zero rows come back.
- The session-variable wiring. Either set
app.current_tenantin a middleware that runs on every request, or use a connection pool that scopes per request. - A clear admin path that bypasses RLS. Your own staff (and your superadmin tools) need a separate role that can see across tenants. Mark it explicitly. Audit it.
- Tenant onboarding and offboarding flows. Spinning up a new tenant, archiving an old one. Plan it now — not when the first customer asks to leave.
Billing quotas — the second silent killer
Multi-tenant SaaS that bills customers has a second hard problem: enforcing what each tenant is allowed to do based on their plan. Users on the Pro plan get 100,000 API calls a month; users on the Free plan get 5,000. Whose job is it to enforce that?
Three components you need on day one:
- A plan definition table that maps each plan to its limits.
- A usage counter per tenant, ideally in Redis for speed, materialised back to Postgres for billing.
- A middleware that rejects or throttles requests when the limit is hit.
Build this before you launch paid plans. Adding it later means refactoring every API endpoint and every billing webhook.
The mistakes we have watched (and rescued)
- No tenant_id on early tables. A founder built a single-tenant MVP, raised seed, signed a second customer. The data leak risk was so severe the team had to spend two months retrofitting RLS before they could onboard.
- tenant_id but no RLS. Application-level filtering only. One forgotten WHERE clause in an admin endpoint leaked customer data across tenants. Postgres RLS would have prevented it.
- Database-per-tenant with no automation. Manually creating a new database per tenant worked for 5 customers. By 50 customers, the team was spending half its time on database operations.
Multi-tenancy is not a feature you add when the second customer signs. It is an architecture you build on day one. Retrofitting it costs more than building it correctly from scratch.
What we ship for every multi-tenant SaaS
On every multi-tenant SaaS we build at Vybix:
- Shared-schema architecture with
tenant_idon every tenant-scoped table. - Postgres row-level security policies on every table, tested with integration tests that assert isolation.
- Tenant onboarding flow — a single API call that creates the tenant, seeds default data, and sends the invite email.
- A plan / usage / quota system wired in from week 8.
- Stripe (or Razorpay) subscription billing with idempotent webhook handling, dunning, and plan upgrade/downgrade logic.
- A superadmin tool that lets us debug a specific tenant's data with explicit, audited access.
The bottom line
If you are building anything multi-tenant — and most B2B SaaS is multi-tenant — get the foundation right on day one. Postgres RLS, tenant_id discipline, billing quotas, and tenant lifecycle flows. The week of extra thought up front prevents the quarter of expensive retrofitting later. See our SaaS development service for how we structure this on real projects.