Multi-Tenancy in SaaS: The Architecture Decision You Can't Undo Later
Most early architecture decisions in a SaaS product are reversible. Wrong framework? Migrate it. Wrong hosting provider? Move it. Wrong multi-tenancy model? That one's expensive to fix once you have real customers and real data sitting in production.
If you're building a SaaS platform and haven't explicitly decided how tenant data is isolated, you've actually already made the decision by default — usually the one that's easiest to type, not the one that scales best. Here's a practical breakdown of the three real options, when each one makes sense, and what we typically default to when architecting SaaS platforms on PostgreSQL, MongoDB, and AWS.
The Three Real Options
1. Shared Database, Shared Schema (Row-Level Isolation)
Every tenant's data lives in the same tables, distinguished by a tenant_id column.
CREATE TABLE invoices (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL REFERENCES tenants(id),
amount NUMERIC NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX idx_invoices_tenant ON invoices(tenant_id);
Pros: Cheapest to run, simplest migrations (one schema to update, ever), easiest to query across tenants for internal analytics.
Cons: One bad query without a WHERE tenant_id = ? clause and you've got a serious data leak. This is the model where a single missing filter is a headline-worthy incident, not a minor bug.
The fix most teams reach for is enforcing isolation at the database level instead of trusting every developer to remember the filter every time — Postgres Row-Level Security does exactly that:
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON invoices
USING (tenant_id = current_setting('app.current_tenant')::uuid);
Set app.current_tenant at the start of every request (from the authenticated session), and Postgres enforces isolation even if a developer forgets the WHERE clause. This single feature is, in our experience, the difference between "safe shared-schema multi-tenancy" and "multi-tenancy that's one code review miss away from a very bad day."
2. Shared Database, Schema-per-Tenant
Each tenant gets their own Postgres schema within the same database:
CREATE SCHEMA tenant_acme;
CREATE SCHEMA tenant_globex;
Pros: Stronger isolation than row-level, still relatively cheap to run since it's one physical database. Backups and per-tenant data export are cleaner.
Cons: Migrations become a loop instead of a single command — you're now running your migration script against every tenant schema, which gets slow and error-prone past a few hundred tenants. Connection pooling also gets trickier since schema-switching happens per request.
This tends to be the right call for B2B SaaS with a moderate number of larger customers (tens to low hundreds), especially where customers explicitly ask about data isolation for compliance reasons — schema-per-tenant is much easier to explain in a security questionnaire than "we use a WHERE clause."
3. Database-per-Tenant
Each tenant gets a fully separate database, sometimes even a separate instance.
Pros: The strongest isolation available short of separate infrastructure entirely. Easiest model to reason about for compliance-heavy industries (health, finance, government contracts) and the only option in this list where "delete this tenant's data completely" is a single DROP DATABASE rather than a targeted deletion job you have to trust.
Cons: Expensive, and operationally heavier — connection management, monitoring, and backups all multiply per tenant. Cross-tenant analytics (e.g. "average usage across all customers") requires a separate data pipeline rather than a simple query.
This is usually reserved for enterprise tiers — a single "Enterprise" plan tenant gets database-per-tenant while your standard tiers stay on shared-schema, giving you the best of both without paying enterprise infrastructure costs for every free-tier signup.
Where MongoDB Fits
For SaaS products with more flexible, document-shaped data (activity feeds, configuration objects, event logs), we often pair Postgres for core transactional data (billing, users, tenants) with MongoDB for high-volume, schema-flexible data. Tenant isolation in Mongo typically follows the same three patterns — a tenantId field with proper indexing for the shared-collection model, or separate collections/databases per tenant for stronger isolation:
db.events.createIndex({ tenantId: 1, createdAt: -1 });
db.events.find({ tenantId: currentTenant, createdAt: { $gte: startOfMonth } });
The same rule applies here as in Postgres: never trust application code alone to filter by tenant on every query. Build the check as close to the data layer as possible — a repository/query-builder layer that automatically injects the tenant filter is far safer than hoping every engineer remembers it in every service file.
File Storage: Isolating Tenants in S3
This one gets overlooked constantly. If tenants upload files — documents, images, exports — namespace every object by tenant from day one:
s3://your-app-uploads/{tenant_id}/{resource_type}/{file_id}.pdf
Combine this with per-tenant scoped IAM policies or presigned URLs generated server-side (never client-side) so a tenant can only ever generate a signed URL for objects under their own prefix. Retrofitting this after tenants already have thousands of files under a flat structure is a genuinely painful migration — worth getting right before your first real customer uploads anything.
So Which One Should You Actually Pick?
If you're building an MVP and don't yet know your customer profile: start with shared schema + Row-Level Security. It's the cheapest to run, fast to build, and RLS closes off the scariest failure mode. You can migrate specific large or compliance-sensitive tenants to their own schema or database later — this is a much easier migration to do selectively than to do a full switch of your isolation model after the fact.
If you already know you're selling to enterprise or regulated industries from day one, it's worth designing for schema-per-tenant or database-per-tenant from the start, even though it's more setup — retrofitting isolation once you have production data and paying customers is a significantly bigger project than building it in from the beginning.
The Real Takeaway
Multi-tenancy isn't a checkbox — it's a decision that touches your database schema, your query layer, your file storage, your backup strategy, and eventually your compliance story. The teams that get burned are almost always the ones that let the isolation model happen implicitly (usually shared-schema with no RLS and a prayer that nobody forgets a WHERE clause) instead of choosing it deliberately.
Pick the model that matches who you're selling to today, build in the database-level safety net regardless of which model you choose, and revisit the decision explicitly once your customer base or compliance requirements change — not after an incident forces the conversation.
I work on SaaS architecture at Macro Gen, a Christchurch-based dev studio building SaaS platforms end-to-end on Postgres, MongoDB, and AWS. If you're mid-decision on your own multi-tenancy model, happy to compare notes — drop a comment or find us on Facebook.
Top comments (0)