Performance

PostgreSQL Schema Design Patterns for Multi-Tenant Applications

Concrete schema design, composite indexing strategy, row-level security setup, and the migration approach for a production multi-tenant PostgreSQL database.

admin · April 30, 2026 · 2 min read

Choosing Shared Schema with Row-Level Security

For B2B SaaS with a large number of small-to-medium customers, shared schema with PostgreSQL row-level security enforced at the database level is our default recommendation. Application-level WHERE tenant_id = ? filtering is a liability — a single forgotten clause is a data leak. Database-level RLS eliminates this class of bug entirely.

RLS Setup

Enable RLS on every tenant-data table: ALTER TABLE orders ENABLE ROW LEVEL SECURITY. Create the isolation policy: CREATE POLICY tenant_isolation ON orders USING (tenant_id = current_setting(‘app.tenant_id’)::uuid). Set the session variable on every database connection before executing queries: SET LOCAL app.tenant_id = ‘the-tenant-uuid’. With this configuration, a query that omits a WHERE tenant_id clause still returns only the current tenant’s data.

Indexing Strategy

Every tenant-data table requires a composite index with tenant_id as the leading column. A composite index on (tenant_id, created_at DESC) for the common “latest records for this tenant” query pattern. All additional indexes are also composite — (tenant_id, email) not just (email) — because a single-column index would return rows from all tenants, requiring a filter step the planner cannot push into the index scan.

Safe Migration Patterns

New NOT NULL columns must have a DEFAULT value or be added as nullable and backfilled in a separate transaction before adding the NOT NULL constraint. Never add a NOT NULL column without a default to a large production table — PostgreSQL rewrites every row in the table while holding an exclusive lock. For tables with millions of rows, this means minutes or hours of unavailability.