Choosing the right multi-tenant schema pattern in PostgreSQL has major implications for data isolation, query performance, and operational complexity. Here are the three main patterns.
Pattern 1: Shared Schema (Tenant ID Column)
-- All tenants share tables; tenant_id column separates rows
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
tenant_id UUID NOT NULL REFERENCES tenants(id),
amount NUMERIC(10,2),
created_at TIMESTAMPTZ DEFAULT now()
);
-- Composite index for tenant-scoped queries
CREATE INDEX idx_orders_tenant_created ON orders (tenant_id, created_at DESC);
-- Combine with RLS for enforcement
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON orders
USING (tenant_id = current_setting('app.tenant_id')::uuid);Pros: Simple ops, shared indexes, easy to add tenants
Cons: Must ensure tenant isolation at app/RLS layer; noisy neighbor risk
Pattern 2: Schema per Tenant
-- Each tenant gets their own schema
CREATE SCHEMA tenant_abc123;
CREATE TABLE tenant_abc123.orders (
id BIGSERIAL PRIMARY KEY,
amount NUMERIC(10,2),
created_at TIMESTAMPTZ DEFAULT now()
);
-- Set search_path per connection
SET search_path = tenant_abc123;Pros: Strong isolation, easy per-tenant backup/restore
Cons: Schema migrations must run per-tenant; hard to query across tenants; PostgreSQL limit ~10K schemas before performance degrades
Pattern 3: Database per Tenant
Each tenant gets a separate PostgreSQL database (or RDS instance).
Pros: Maximum isolation, independent scaling
Cons: High operational overhead; cross-tenant queries impossible; connection pooling complexity multiplied
Table Partitioning for Shared Schema at Scale
-- Partition orders by tenant for large multi-tenant tables
CREATE TABLE orders (
id BIGSERIAL,
tenant_id UUID NOT NULL,
amount NUMERIC(10,2),
created_at TIMESTAMPTZ
) PARTITION BY HASH (tenant_id);
CREATE TABLE orders_p0 PARTITION OF orders FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE orders_p1 PARTITION OF orders FOR VALUES WITH (MODULUS 4, REMAINDER 1);
CREATE TABLE orders_p2 PARTITION OF orders FOR VALUES WITH (MODULUS 4, REMAINDER 2);
CREATE TABLE orders_p3 PARTITION OF orders FOR VALUES WITH (MODULUS 4, REMAINDER 3);Key Takeaways
- Shared schema + RLS is the most operationally scalable pattern for most SaaS applications
- Schema-per-tenant provides stronger isolation but limits you to ~1,000 tenants before ops complexity explodes
- Always index
(tenant_id, primary_sort_column)for shared-schema queries - Hash partitioning on
tenant_iddistributes I/O for very large shared-schema deployments
JusDB Can Help
Choosing the wrong multi-tenant architecture is costly to change later. JusDB can help you design the right schema pattern for your SaaS workload from day one.