Multi-Tenant SaaS Architecture with PostgreSQL: Tenant Isolation That Holds
Multi-tenant SaaS architecture with PostgreSQL: shared schema vs schema or database per tenant, row-level security, tenant resolution and real pitfalls.
Full-Stack, Mobile & AI Engineer
Every B2B SaaS product eventually has to answer one question perfectly: can a customer ever see another customer's data? In a multi-tenant system, where many customers share the same application and database, the answer depends on architecture decisions made early, and on discipline enforced everywhere afterwards.
This guide covers multi-tenant SaaS architecture with PostgreSQL: the three isolation models, how row-level security turns isolation into a database guarantee, how to resolve the current tenant, and the operational pitfalls (noisy neighbours, migrations, exports) that appear as you grow. I built Floey's multi-tenant backend, where each gym is an isolated tenant with its own branded booking site, staff roles and analytics, so the examples come from that kind of product.
Key takeaways
- Multi-tenancy means many customers share one system, with each tenant's data strictly isolated from the others.
- Shared schema with a
tenant_idon every table is the most common model and scales to many tenants; schema-per-tenant and database-per-tenant trade efficiency for stronger separation. - PostgreSQL row-level security (RLS) enforces isolation in the database, so a forgotten
WHERE tenant_id = …can't leak data. - Resolve the tenant once per request (from the domain, a header or the token) and carry it through every query.
- Plan for per-tenant operations early: limits, exports, deletion, analytics and migrations.
What multi-tenant SaaS looks like
Familiar examples of multi-tenancy are Slack workspaces, Shopify stores and project-management tools where each company has its own space. Each customer ("tenant") sees only their own data, users and settings, while the product runs on shared infrastructure. In Floey's case, each gym is a tenant: its classes, members, staff, bookings and payouts belong to that gym alone.
Multi-tenant vs single-tenant vs multi-instance
| Multi-tenant | Single-tenant | Multi-instance | |
|---|---|---|---|
| What it means | One application and database serve many customers, isolated by tenant | Each customer gets their own application and database | Separate copies of the same application per customer or region, managed together |
| Cost per customer | Lowest | Highest | Middle |
| Isolation | Logical (enforced in code and the database) | Physical | Physical per instance |
| Upgrades | One deployment for everyone | Per customer | Per instance, usually automated |
| Typical use | Most SaaS products | Regulated or very large enterprise customers | Data-residency requirements, large customers |
Most products start multi-tenant and add single-tenant or regional instances later for the customers who need them.
The three isolation models
| Model | How it works | Strengths | Weaknesses |
|---|---|---|---|
| Shared schema | All tenants in the same tables, each row has tenant_id | Efficient, simple to operate, scales to thousands of tenants | Isolation depends on every query (unless RLS enforces it) |
| Schema per tenant | Each tenant gets its own Postgres schema | Clearer separation, per-tenant backups are easier | Migrations run per schema; many schemas get unwieldy |
| Database per tenant | Each tenant has its own database | Strongest isolation, custom placement | Most expensive and complex to operate |
Most products should start with a shared schema plus row-level security, and reserve separate databases for the few customers who contractually need them.
Row-level security: isolation the database enforces
With RLS, PostgreSQL itself filters rows by tenant, based on a setting your application provides per transaction:
ALTER TABLE bookings ENABLE ROW LEVEL SECURITY;
ALTER TABLE bookings FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON bookings
USING (tenant_id = current_setting('app.current_tenant')::uuid)
WITH CHECK (tenant_id = current_setting('app.current_tenant')::uuid);At the start of each request's transaction, the application sets the tenant:
SET LOCAL app.current_tenant = '6f1c2a0e-...';Now every query on bookings sees only that tenant's rows, and inserts can't write rows for another tenant, even if a developer forgets a filter. Two details matter: connect with a database role that isn't a superuser and doesn't bypass RLS, and use SET LOCAL inside a transaction so the setting can't leak to another request through a pooled connection.
Resolving the current tenant
| Source | Example | Good for |
|---|---|---|
| Domain or subdomain | A gym's branded booking site | Public, per-tenant websites |
| Token claims | The signed-in staff member's tenant | Apps and dashboards |
| Explicit header or path | /t/{tenant}/... | APIs and admin tools |
Resolve it once, in middleware, verify that the user actually belongs to that tenant, then pass it to the database layer. Never accept a tenant ID from the client without checking it against the user's memberships.
From the field: For Floey in Kuwait I built all three products in the suite: the consumer app (iOS, Android and web), Floey Business for gym owners and staff, and the multi-tenant Django backend serving both, where each gym is an isolated tenant with its own branded booking site, staff roles and analytics. A consumer app that spans many gyms on top of strictly isolated gym data is exactly the shape multi-tenancy is designed for.
When one tenant outgrows the shared model
Sometimes a single large customer needs its own database: for contractual data residency, very high load or custom retention rules. Design for that possibility without building it on day one. If every query already runs through a tenant context, moving one tenant to a dedicated database becomes a routing decision (which connection to use for this tenant) plus a data migration, rather than a rewrite of the application.
How to build a multi-tenant SaaS application: a checklist
- Make the tenant a first-class concept in the data model from day one: a
tenantstable and atenant_idon every tenant-owned row. - Resolve the tenant once per request from the authenticated user, never from a client-supplied header alone.
- Enforce isolation in the database with row-level security, so a missed
WHEREclause can't leak data. - Scope everything else by tenant too: file storage paths, caches, search indexes, background jobs and logs.
- Add tenant-aware billing and limits (plans, seats, usage) early, because they shape the data model.
- Test isolation explicitly: automated tests that try to read another tenant's data and must fail.
Multi-tenant SaaS on AWS
On AWS, a common shape is PostgreSQL on RDS or Aurora with row-level security, an identity provider that puts the tenant into the user's token claims, per-tenant prefixes in S3, and tenant IDs on every log line and metric so you can see one customer's usage and errors. AWS's Well-Architected SaaS Lens covers the trade-offs between pooled and siloed resources in depth.
Roles within a tenant
Isolation between tenants is one layer; permissions within a tenant are another. A gym has owners, managers, front-desk staff and instructors, each allowed different actions. Model roles per tenant membership (a person can be an instructor at one gym and a member at another), and check permissions in the application layer on top of database isolation.
Operational pitfalls as you grow
- Noisy neighbours: one large tenant can slow everyone down. Add per-tenant rate limits and watch per-tenant query load.
- Migrations: with schema-per-tenant, every migration runs many times; with a shared schema, large tables need careful, online migrations.
- Indexes: most queries filter by tenant, so
tenant_idusually leads composite indexes. - Exports and deletion: customers will ask for their data and for deletion when they leave. Build tenant-scoped export and delete jobs early.
- Analytics: per-tenant dashboards are a selling point; platform-wide analytics must respect the same isolation.
- Background jobs: every job must carry and set the tenant context, or it bypasses the isolation you built for requests.
FAQ
What is multi-tenant SaaS architecture?
It's a design where one application and its infrastructure serve many customers (tenants), with each tenant's data, users and settings isolated from the others. Common implementations use a shared database with a tenant ID on every row, a schema per tenant, or a database per tenant.
What's an example of a multi-tenant SaaS?
Slack workspaces, Shopify stores and booking platforms where each business has its own space are classic examples. In a fitness booking platform, each gym is a tenant with its own classes, members, staff and payments, all running on shared infrastructure.
Should I use row-level security for multi-tenancy?
For shared-schema designs on PostgreSQL, it's strongly recommended. Row-level security makes the database enforce tenant isolation, so a missing filter in application code can't expose another tenant's data. Use a non-superuser role and set the tenant per transaction.
Schema per tenant or shared schema?
Start with a shared schema plus row-level security for most products, because it's efficient and scales to many tenants. Schema per tenant suits a smaller number of larger tenants needing clearer separation; database per tenant is for customers with strict contractual isolation needs.
Next steps
If you're designing a multi-tenant SaaS, or worried your current isolation relies on every developer remembering a filter, I can review the architecture and add database-enforced isolation safely. Get in touch, or see how I work as a full-stack developer.
Sources
- SaaS Lens, AWS Well-Architected
- Row security policies, PostgreSQL docs
- CREATE POLICY, PostgreSQL docs
- Multi-tenant SaaS architecture (SaaS Lens), AWS Well-Architected
- django-tenants, GitHub
Related
The Backend Your Mobile App Actually Needs (and What to Skip)
What backend a mobile app really needs in 2026: auth, API, database, files, push, payments and admin, managed vs custom, and what to skip at the start.
Django REST Framework vs FastAPI for Mobile Backends
Django REST Framework vs FastAPI for a mobile app backend in 2026: batteries vs speed, async, admin, ORM, typing and which fits your product and team.
Real-Time Chat at Scale with Socket.IO and Redis Pub/Sub
Build real-time chat that scales with Socket.IO and Redis: rooms, the Redis adapter, delivery and ordering, presence, reconnection and React Native.
