Multi-tenancy database patterns and strategies

Maria Garcia Feb 2026
2 tabs
-- Pattern 1: Shared schema with tenant_id column

CREATE TABLE tenants (
  id SERIAL PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  slug VARCHAR(50) UNIQUE NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  tenant_id INT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
  username VARCHAR(50) NOT NULL,
  email VARCHAR(100) NOT NULL,
  UNIQUE (tenant_id, email)
);

CREATE INDEX idx_users_tenant ON users(tenant_id);

CREATE TABLE orders (
  id SERIAL PRIMARY KEY,
  tenant_id INT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
  user_id INT NOT NULL REFERENCES users(id),
  total DECIMAL(10,2),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  CONSTRAINT fk_order_user_tenant
    FOREIGN KEY (tenant_id, user_id)
    REFERENCES users(tenant_id, id)  -- Ensures same tenant
);

CREATE INDEX idx_orders_tenant ON orders(tenant_id);

-- All queries MUST include tenant_id
SELECT * FROM users
WHERE tenant_id = 123 AND email = 'user@example.com';

-- Set application context (connection-level)
SET app.current_tenant_id = '123';

-- Application ensures tenant_id in all queries
INSERT INTO users (tenant_id, username, email)
VALUES (
  current_setting('app.current_tenant_id')::INT,
  'newuser',
  'new@example.com'
);

-- Pattern 2: Row-Level Security (automatic filtering)

ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

-- Policy: Users only see their tenant's data
CREATE POLICY tenant_isolation_users ON users
  USING (tenant_id = current_setting('app.current_tenant_id')::INT);

CREATE POLICY tenant_isolation_orders ON orders
  USING (tenant_id = current_setting('app.current_tenant_id')::INT);

-- Now queries automatically filter by tenant
SET app.current_tenant_id = '123';
SELECT * FROM users;  -- Only tenant 123's users

-- Superuser can see all (for admin queries)
CREATE POLICY admin_all_users ON users
  FOR ALL
  TO admin_role
  USING (true);

-- Force RLS even for table owner
ALTER TABLE users FORCE ROW LEVEL SECURITY;

-- Pattern 3: Separate schema per tenant

-- Create schema for each tenant
CREATE SCHEMA tenant_123;
CREATE SCHEMA tenant_456;

-- Create tables in each schema
CREATE TABLE tenant_123.users (
  id SERIAL PRIMARY KEY,
  username VARCHAR(50),
  email VARCHAR(100)
);

CREATE TABLE tenant_456.users (
  id SERIAL PRIMARY KEY,
  username VARCHAR(50),
  email VARCHAR(100)
);

-- Set search path per connection
SET search_path TO tenant_123, public;
SELECT * FROM users;  -- Queries tenant_123.users

-- Schema creation function
CREATE OR REPLACE FUNCTION create_tenant_schema(tenant_slug VARCHAR)
RETURNS VOID AS $$
BEGIN
  EXECUTE format('CREATE SCHEMA %I', tenant_slug);

  EXECUTE format('
    CREATE TABLE %I.users (
      id SERIAL PRIMARY KEY,
      username VARCHAR(50),
      email VARCHAR(100) UNIQUE
    )', tenant_slug);

  EXECUTE format('
    CREATE TABLE %I.orders (
      id SERIAL PRIMARY KEY,
      user_id INT REFERENCES %I.users(id),
      total DECIMAL(10,2)
    )', tenant_slug, tenant_slug);

  -- Grant permissions
  EXECUTE format('GRANT USAGE ON SCHEMA %I TO app_user', tenant_slug);
  EXECUTE format('GRANT ALL ON ALL TABLES IN SCHEMA %I TO app_user', tenant_slug);
END;
$$ LANGUAGE plpgsql;

-- Create new tenant
INSERT INTO tenants (name, slug) VALUES ('Acme Corp', 'acme');
SELECT create_tenant_schema('acme');

-- Schema migrations for all tenants
DO $$
DECLARE
  tenant_record RECORD;
BEGIN
  FOR tenant_record IN SELECT slug FROM tenants LOOP
    EXECUTE format('
      ALTER TABLE %I.users ADD COLUMN IF NOT EXISTS phone VARCHAR(20)
    ', tenant_record.slug);
  END LOOP;
END $$;
2 files · sql Explain with highlit

Multi-tenancy serves multiple customers from one application. I implement tenant isolation via schemas, databases, or row-level security. Shared schema with tenant_id column is simplest—good indexing essential. Separate schemas per tenant improves isolation, complicates migrations. Separate databases provide maximum isolation but expensive. Row-level security enforces automatic filtering. Understanding tenant data size guides strategy. Connection pooling per tenant prevents resource exhaustion. Tenant context must be set per request. Backup and restore strategies differ by approach. Proper multi-tenancy balances isolation, performance, operational complexity. Essential for SaaS applications, B2B platforms. PostgreSQL RLS makes shared-schema multi-tenancy secure and efficient.