Multi-Tenant Basics
8 examples to get you started with Multi-Tenant patterns - 5 basic and 3 intermediate.
Search across all documentation pages
8 examples to get you started with Multi-Tenant patterns - 5 basic and 3 intermediate.
The simplest pattern: tenant_id on all tenant-owned rows.
CREATE TABLE tenants (
tenant_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL,
plan text NOT NULL DEFAULT 'starter'
);
CREATE TABLE projects (
project_id bigint GENERATED ALWAYS AS IDENTITY,
tenant_id uuid NOT NULL REFERENCES tenants (tenant_id),
name text NOT NULL,
PRIMARY KEY (tenant_id, project_id)
);(tenant_id, project_id) scopes identity per tenant.tenant_id - missing filter risks cross-tenant leaks.tenant_id for OLTP lookups.Related: Shared Schema + tenant_id - indexing and query patterns
Uniqueness applies per tenant, not globally.
CREATE TABLE projects (
tenant_id uuid NOT NULL,
project_id bigint GENERATED ALWAYS AS IDENTITY,
slug text NOT NULL,
PRIMARY KEY (tenant_id, project_id),
UNIQUE (tenant_id, slug)
);UNIQUE (tenant_id, slug) lets two tenants use slug api.UNIQUE (slug) breaks multi-tenant SaaS.Set tenant once per connection or transaction for consistent filtering.
-- Application sets after auth
SET app.tenant_id = 'f47ac10b-58cc-4372-a567-0e02b2c3d479';
SELECT project_id, name FROM projects
WHERE tenant_id = current_setting('app.tenant_id')::uuid;app.tenant_id) centralizes tenant context.WHERE clauses are safe.DISCARD ALL or RESET ALL).Related: Row-Level Security Tenancy - policy templates
Find tenants consuming disproportionate resources.
SELECT
tenant_id,
COUNT(*) AS row_count
FROM audit_events
WHERE logged_at > now() - interval '1 hour'
GROUP BY tenant_id
ORDER BY row_count DESC
LIMIT 10;tenant_id counts signal noisy neighbors on shared schema.pg_stat_statements grouped by tenant when tagged in comments.Align indexes with WHERE tenant_id = ? access paths.
CREATE INDEX projects_tenant_name_idx ON projects (tenant_id, name);
CREATE INDEX audit_events_tenant_time_idx
ON audit_events (tenant_id, logged_at DESC);tenant_id enables index scans per tenant.(tenant_id, logged_at DESC).tenant_id in indexes causes seq scans within large shared tables.Database-enforced tenant isolation as a safety net.
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
ALTER TABLE projects FORCE ROW LEVEL SECURITY;
CREATE POLICY projects_tenant_isolation ON projects
USING (tenant_id = current_setting('app.tenant_id', true)::uuid)
WITH CHECK (tenant_id = current_setting('app.tenant_id', true)::uuid);FORCE ROW LEVEL SECURITY applies policies even to table owners.WITH CHECK blocks inserts into wrong tenant.Related: Row-Level Security Tenancy - tier templates
Stronger namespace isolation for mid-market enterprise tier.
CREATE SCHEMA tenant_acme;
CREATE SCHEMA tenant_globex;
CREATE TABLE tenant_acme.invoices (
invoice_id bigint PRIMARY KEY,
amount numeric(12, 2) NOT NULL
);search_path or qualified names route queries.Related: Schema-per-Tenant - trade-offs
Hard isolation for compliance-heavy customers.
CREATE DATABASE tenant_acme_db;
CREATE DATABASE tenant_globex_db;
\c tenant_acme_db
CREATE TABLE invoices (
invoice_id bigint PRIMARY KEY,
amount numeric(12, 2) NOT NULL
);Related: Database-per-Tenant - enterprise isolation
Stack versions: This page was written for PostgreSQL 18.4 (stable 18, maintenance 17), pgvector 0.8+, PgBouncer 1.x, Patroni 3.x, and PostGIS 3.5+.
Reviewed by Chris St. John·Last updated Jul 18, 2026