Multi-Tenant SaaS Architecture: Which Model Should You Choose?
For most early-stage SaaS products, the right starting point is a shared database with a shared schema: every customer's rows live in the same tables, each row carries a tenant_id, and the database itself enforces isolation with PostgreSQL row-level security. Move individual customers to schema-per-tenant or database-per-tenant only when something concrete forces it: a contract, a regulation, a customer large enough to disrupt everyone else, or a per-customer restore requirement.
That is the short answer. The rest of this guide explains the three models, how to build the default one safely, and the signals that tell you when to change course. It is written for founders and CTOs making this decision early, when it is cheap to get right and expensive to fix later. It also feeds directly into budget; see how much it costs to build a SaaS product.
What multi-tenancy actually means
In a multi-tenant SaaS, one running application serves many customers (tenants) from shared infrastructure, while each tenant's data and configuration stay invisible to the others. The alternative, single-tenant, gives each customer their own deployment. Single-tenant is simpler to reason about but multiplies your hosting and operations work with every new customer, which is why most SaaS products start multi-tenant.
The real design question is how you separate tenants' data. There are three common models.
The three tenancy models
1. Shared database, shared schema (the "pool" model).
All tenants use the same tables. Every tenant-owned row has a tenant_id column.
- Strengths: cheapest to run, simplest to migrate (one schema change applies to everyone), scales to thousands of small tenants, easiest to build analytics across tenants.
- Weaknesses: isolation depends on your code and policies getting every query right; one heavy tenant can slow the others; restoring a single tenant's data from a backup is awkward.
2. Shared database, schema per tenant (the "bridge" model). One database, but each tenant gets its own PostgreSQL schema containing its own copy of the tables.
- Strengths: stronger logical separation, cleaner per-tenant export and deletion.
- Weaknesses: every migration must run once per tenant, so 500 tenants means 500 migrations; catalog size and tooling get heavier as tenant count grows. Some teams recommend avoiding this model entirely, because it carries much of the operational cost of database-per-tenant without the isolation guarantees regulators or enterprise security teams often ask for.
3. Database per tenant (the "silo" model). Each tenant gets a separate database, sometimes separate infrastructure.
- Strengths: strongest isolation, per-tenant backup and restore, easy data residency, customer-specific tuning.
- Weaknesses: highest cost per tenant, provisioning and migration complexity, harder cross-tenant reporting, and connection management gets difficult at scale.
Which one should you choose?
Start with shared schema unless one of these is true on day one:
- You sell to regulated industries where contracts or auditors require physical data separation.
- Your first customers are large enterprises that will demand dedicated databases as a condition of the deal.
- Data residency rules require different tenants' data in different regions.
If none of those applies, the pool model keeps your costs and complexity low while you find product-market fit. A common mistake is the opposite one: choosing database-per-tenant "to be safe" before there are ten customers, then spending months on provisioning and migration tooling instead of product.
The best long-term setup is usually tiered: most tenants share a database, and a few large or regulated tenants are moved to their own. To make that possible later, add a small tenant registry now (a table mapping each tenant to where its data lives) and route every request through it, even if every tenant currently points to the same database.
Building the shared-schema model safely
Shared schema is only as safe as its enforcement. Relying on developers to remember WHERE tenant_id = ... in every query will fail eventually. Add the database as a second line of defense.
1. Put tenant_id on every tenant-owned table, and lead indexes with it
Almost every query will filter by tenant, so make tenant_id the first column of your composite indexes. Make uniqueness tenant-scoped too: a unique constraint on email alone means one customer's user can block another customer's sign-up. Use UNIQUE (tenant_id, email) instead.
2. Enforce isolation with PostgreSQL row-level security
When row-level security (RLS) is enabled on a table, normal reads and writes must be permitted by a policy, and with no policy the default is to deny everything. A minimal setup looks like this:
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
ALTER TABLE projects FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON projects
USING (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::uuid)
WITH CHECK (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::uuid);
Two details matter here, and both come straight from how PostgreSQL works:
- Table owners normally bypass RLS, and superusers and roles with the
BYPASSRLSattribute always do.FORCE ROW LEVEL SECURITYmakes the table owner subject to policies, but the safest approach is for your application to connect as a dedicated, unprivileged role that is neither the owner norBYPASSRLS. Keep migrations on a separate, privileged role. - The
NULLIF(...)guard avoids a cast error when the setting is empty, and means a request with no tenant context sees no rows rather than crashing or, worse, seeing everything.
3. Set the tenant context per transaction, not per connection
Applications use connection pools, and a connection is reused by many requests. If you set the tenant once on a connection, the next request on that connection can inherit it. Set it inside each transaction, scoped to that transaction only:
import { Pool, PoolClient } from "pg";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export async function withTenant<T>(
tenantId: string,
fn: (client: PoolClient) => Promise<T>
): Promise<T> {
const client = await pool.connect();
try {
await client.query("BEGIN");
// third argument true = local to this transaction
await client.query("SELECT set_config('app.tenant_id', $1, true)", [tenantId]);
const result = await fn(client);
await client.query("COMMIT");
return result;
} catch (err) {
await client.query("ROLLBACK");
throw err;
} finally {
client.release();
}
}
The tenant ID must come from the authenticated session or token on the server, never from a request parameter the client can change.
4. Test isolation explicitly
Write automated tests that create two tenants, then assert that tenant A cannot read, update or delete tenant B's rows through every API route. Run them in CI. Isolation bugs rarely show up in manual testing.
Where tenant leaks actually happen
The database is usually the best-protected layer. Leaks tend to happen elsewhere:
- Caches. A cache key like
user:42:settingswithout the tenant in it is a leak waiting for an ID collision. - Background jobs. Jobs that run outside a request need the tenant context passed in explicitly and applied the same way.
- File storage. Use a tenant-specific prefix in object storage paths and generate signed URLs per request.
- Search indexes and vector stores. If you build AI search or retrieval, every document must carry tenant metadata and every query must filter on it. See our guides on building a production RAG pipeline and how to choose a vector database, and how to add AI to an existing SaaS product for permission-aware retrieval.
- Logs and analytics. Make sure sensitive tenant data does not end up in logs that every engineer or vendor can read.
- Admin and support tools. These deliberately cross tenant boundaries, so they need their own access controls and audit trail.
Handling the noisy-neighbor problem
In a shared database, one tenant running a heavy report can slow everyone else. The standard mitigations are cheap to add early:
- Per-tenant rate limits on API requests
- Quotas on storage, records or expensive operations, tied to plan tier
- Statement timeouts and connection limits so a single request cannot hog the pool
- Moving heavy reporting to a read replica or a separate worker queue
- Fair scheduling in background job queues, so one tenant's bulk import does not starve everyone's emails
If a single tenant keeps causing trouble despite these, that is the signal to move them to their own database.
Signals that it is time to isolate a tenant
Treat these as triggers, not theory:
- A signed contract or security questionnaire requires dedicated data storage
- A regulation or customer requires data to stay in a specific region
- One customer's load is a large share of your database and quotas are not enough
- A customer needs point-in-time restore of only their data
- A customer needs custom schema changes or extensions
When one of these appears, you migrate that tenant only, using the registry you set up earlier. Everyone else stays on the shared database.
Common mistakes
- Choosing the heaviest model first. Database-per-tenant before product-market fit trades product progress for infrastructure work.
- Relying on application filters alone. One forgotten
WHEREclause is a data breach. Use RLS as a backstop. - Connecting as a role that bypasses RLS. Policies that exist but never apply give a false sense of safety.
- Tenant-blind unique constraints and caches. Both fail in subtle, intermittent ways.
- Retrofitting tenancy later. Adding
tenant_idto a mature schema, backfilling it, and auditing every query is slow, risky work. It is far cheaper to build it in from the first migration.
Bottom line
Start with a shared schema, enforce isolation in the database with row-level security, set tenant context per transaction, and build a tenant registry so you can move individual customers to dedicated databases when a real requirement appears. That gives you low cost now and a clean path to enterprise isolation later.
If you are designing a SaaS product and want an architecture review before you commit to a tenancy model, RMJDG's team can look at your requirements and help you choose and implement the right approach.
Services
Not sure where to start? Tell me what you want the product to do.
Related work

Teamlex AI: an AI SEO platform
An AI SEO platform for understanding search intent, analyzing competitors, and creating optimized content.

Parent AI Stories: personalized bedtime stories
A mobile product that helps parents create personalized bedtime stories for children in minutes.