Engineering•5 min read

postgresql-multi-tenant-architecture-guide

LM
Dazzcode Engineering
Software Architect at Dazzcode

When designing a multi-tenant B2B SaaS platform, the single most critical architectural decision is how customer data is isolated within your database layer.

A data breach where one customer views or modifies another tenant's confidential files is fatal to a software company. At the same time, an overly complex data isolation model can dramatically increase operational overhead, complicate database migrations, and skyrocket infrastructure costs.

In this guide, we evaluate the three primary PostgreSQL multi-tenancy patterns, analyze the trade-offs, and provide real implementation code using PostgreSQL Row-Level Security (RLS).


Multi-Tenancy Architecture Comparison

ModelIsolation LevelMigration ComplexityInfrastructure CostMax Tenants (Single Node)Best For
Shared DB + Row-Level Security (RLS)High (Enforced at DB Engine)Very Low (Single schema migration)Lowest (Shared connection pool & cache)100,000+Fast-moving B2B SaaS, Startups, MVPs
Schema-per-TenantVery High (Separate DB schemas)High (Run migrations across N schemas)Moderate (Higher catalog memory overhead)500 – 2,000Mid-market SaaS with custom field requirements
Database-per-TenantComplete (Separate physical DB instances)Very High (Manage N distinct databases)Highest (Dedicated CPU & RAM per client)50 – 200Enterprise / Healthcare / Gov (HIPAA / SOC2 Type II)

Model 1: Shared Database with PostgreSQL Row-Level Security (RLS)

For 95% of modern SaaS platforms built with Next.js, Node.js, or Go, Shared Schema with Row-Level Security (RLS) is the recommended gold standard.

How It Works:

Every table contains a tenantid column. When a user authenticates, your application sets a PostgreSQL session configuration variable (app.currenttenant_id). PostgreSQL then enforces security rules natively for all SELECT, INSERT, UPDATE, and DELETE queries.

sqlBASH/TYPESCRIPT
-- Step 1: Create the multi-tenant table
CREATE TABLE organizations (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE invoices (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
  amount_cents INTEGER NOT NULL,
  status TEXT NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Step 2: Enable Row-Level Security on the table
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;

-- Step 3: Create the isolation policy
CREATE POLICY tenant_isolation_policy ON invoices
  AS RESTRICTIVE
  FOR ALL
  TO authenticated_app_user
  USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid)
  WITH CHECK (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid);

Application Connection Flow:

Before executing any business queries inside a database transaction, your backend runs a parameterized SET LOCAL:

typescriptBASH/TYPESCRIPT
// Next.js / Node.js Transaction Wrapper Example
import { pool } from "@/lib/db";

export async function executeTenantQuery<T>(
  tenantId: string,
  queryFn: (client: any) => Promise<T>
): Promise<T> {
  const client = await pool.connect();
  try {
    await client.query("BEGIN;");
    // Set tenant context for this specific transaction
    await client.query("SET LOCAL app.current_tenant_id = $1;", [tenantId]);
    
    const result = await queryFn(client);
    
    await client.query("COMMIT;");
    return result;
  } catch (error) {
    await client.query("ROLLBACK;");
    throw error;
  } finally {
    client.release();
  }
}

Why We Recommend RLS:

  1. Developer Ergonomics: Even if an engineer writes SELECT * FROM invoices; without a WHERE clause, PostgreSQL strictly filters rows to the active tenant_id. Cross-tenant leaks are mathematically prevented.
  2. Effortless Migrations: Running schema migrations (adding a column, creating an index) executes once across the whole database in seconds.
  3. Optimized Resource Utilization: All tenants share a single buffer pool, connection pool, and query cache.

Model 2: Schema-per-Tenant

In this pattern, all tenants share a single PostgreSQL database instance, but each customer gets a dedicated schema namespace (tenantacme.invoices, tenantglobex.invoices).

Pros:

  • Clean data isolation.
  • Easy to back up or delete a single tenant's data (DROP SCHEMA tenant_acme CASCADE;).
  • Ability to add bespoke custom tables for high-paying clients.

Cons:

  • Migration Nightmare: If you have 1,000 tenants and apply a new migration, your migration runner must execute 1,000 DDL operations sequentially. If a migration fails on tenant 450, you enter a partial failure state.
  • PostgreSQL System Catalog Bloat: PostgreSQL maintains metadata in memory for all tables and indices. Having thousands of schemas can degrade query planning speed.

Model 3: Database-per-Tenant

Each tenant is provisioned an entirely separate PostgreSQL database (or container instance).

When to Use It:

  • Strict regulatory mandates where customer agreements state data cannot share physical storage with competitors.
  • Enterprise clients paying $50,000+ annually who demand custom backup schedules and dedicated point-in-time recovery (PITR).

Essential Performance Optimizations for Multi-Tenant PostgreSQL

1. Compound Indexing with tenant_id Prefix

Every index on a multi-tenant table should almost always include tenant_id as the leading column:

sqlBASH/TYPESCRIPT
-- Optimal index for tenant-scoped date queries
CREATE INDEX idx_invoices_tenant_date ON invoices (tenant_id, created_at DESC);

2. PgBouncer Connection Pooling

Serverless application environments (like Next.js on Vercel or AWS Lambda) spin up hundreds of transient database connections. PostgreSQL allocates ~10MB of RAM per connection. Always place PgBouncer in transaction pooling mode between your application servers and PostgreSQL.

Read our complete guide on PostgreSQL Performance Tuning for SaaS to master connection limits and query plans.


Summary & Next Steps

For modern SaaS teams, PostgreSQL with Row-Level Security offers the best balance of ironclad security, architectural elegance, and rapid development speed.

If you are designing a new SaaS data architecture or looking for a Code Audit to verify your multi-tenant security, the engineering team at Dazzcode is ready to help.

Engineering Partnership

Need Help Implementing This in Your Product?

Dazzcode engineers production-ready SaaS platforms, conducts deep codebase audits, and deploys high-concurrency cloud systems for startups and growing businesses.

LM

Written by Dazzcode Engineering

Lead software systems architect at Dazzcode. Specializing in multi-tenant SaaS architecture, PostgreSQL query tuning, Next.js performance, and scalable cloud engineering.

About Dazzcode Engineering

Ready to Build, Audit or Scale Your SaaS?

Schedule a scoping session with our lead architects to discuss timeline, milestones, and architecture.