Skip to content
Go back

Implementing Multi-Tenant Database Schemas in PostgreSQL

Implementing Multi-Tenant Database Schemas in PostgreSQL

Introduction

Multi-tenancy allows serving multiple customers from a single database. PostgreSQL supports schema-based and shared-table approaches with proper isolation.

Prerequisites

Step 1: Schema-Based Multi-Tenancy

Create separate schema per tenant:

-- Create tenant schemas
CREATE SCHEMA tenant_1;
CREATE SCHEMA tenant_2;

-- Create identical table structure in each schema
CREATE TABLE tenant_1.users (
  id SERIAL PRIMARY KEY,
  name VARCHAR(255) NOT NULL,
  email VARCHAR(255) UNIQUE NOT NULL
);

CREATE TABLE tenant_2.users (
  id SERIAL PRIMARY KEY,
  name VARCHAR(255) NOT NULL,
  email VARCHAR(255) UNIQUE NOT NULL
);

Step 2: Dynamic Schema Switching

import { Pool } from 'pg';

class TenantDatabase {
  private pool: Pool;

  constructor() {
    this.pool = new Pool({
      connectionString: process.env.DATABASE_URL,
    });
  }

  async withTenant<T>(tenantId: string, callback: (client: any) => Promise<T>): Promise<T> {
    const client = await this.pool.connect();
    
    try {
      // Set search path to tenant schema
      await client.query(`SET search_path TO tenant_${tenantId}, public`);
      return await callback(client);
    } finally {
      client.release();
    }
  }

  async getTenantUsers(tenantId: string) {
    return this.withTenant(tenantId, async (client) => {
      const result = await client.query('SELECT * FROM users');
      return result.rows;
    });
  }

  async createTenantUser(tenantId: string, name: string, email: string) {
    return this.withTenant(tenantId, async (client) => {
      const result = await client.query(
        'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *',
        [name, email]
      );
      return result.rows[0];
    });
  }
}

export const tenantDb = new TenantDatabase();

Step 3: Shared-Table with RLS

Alternative approach using single table with tenant isolation:

-- Create shared table with tenant_id
CREATE TABLE shared_users (
  id SERIAL PRIMARY KEY,
  tenant_id VARCHAR(50) NOT NULL,
  name VARCHAR(255) NOT NULL,
  email VARCHAR(255) NOT NULL,
  UNIQUE(tenant_id, email)
);

-- Enable RLS
ALTER TABLE shared_users ENABLE ROW LEVEL SECURITY;

-- Create policy for tenant isolation
CREATE POLICY tenant_isolation ON shared_users
  USING (tenant_id = current_setting('app.current_tenant'));

-- Create policy for inserts
CREATE POLICY tenant_insert ON shared_users
  FOR INSERT WITH CHECK (tenant_id = current_setting('app.current_tenant'));

Step 4: RLS Implementation

import { Pool } from 'pg';

class RLSTenantDatabase {
  private pool: Pool;

  constructor() {
    this.pool = new Pool({
      connectionString: process.env.DATABASE_URL,
    });
  }

  async withTenantContext<T>(tenantId: string, callback: (client: any) => Promise<T>): Promise<T> {
    const client = await this.pool.connect();
    
    try {
      // Set tenant context
      await client.query('SET app.current_tenant = $1', [tenantId]);
      return await callback(client);
    } finally {
      client.release();
    }
  }

  async getUsers(tenantId: string) {
    return this.withTenantContext(tenantId, async (client) => {
      const result = await client.query('SELECT id, name, email FROM shared_users');
      return result.rows;
    });
  }

  async createUser(tenantId: string, name: string, email: string) {
    return this.withTenantContext(tenantId, async (client) => {
      const result = await client.query(
        'INSERT INTO shared_users (tenant_id, name, email) VALUES ($1, $2, $3) RETURNING *',
        [tenantId, name, email]
      );
      return result.rows[0];
    });
  }
}

export const rlsDb = new RLSTenantDatabase();

Step 5: Tenant Management

-- Tenant registry table
CREATE TABLE tenants (
  id VARCHAR(50) PRIMARY KEY,
  name VARCHAR(255) NOT NULL,
  created_at TIMESTAMP DEFAULT NOW(),
  active BOOLEAN DEFAULT true
);

-- Function to create new tenant schema
CREATE OR REPLACE FUNCTION create_tenant_schema(tenant_id VARCHAR(50))
RETURNS void AS $$
BEGIN
  EXECUTE format('CREATE SCHEMA %I', 'tenant_' || tenant_id);
  
  -- Create tables in new schema
  EXECUTE format('
    CREATE TABLE %I.users (
      id SERIAL PRIMARY KEY,
      name VARCHAR(255) NOT NULL,
      email VARCHAR(255) UNIQUE NOT NULL,
      created_at TIMESTAMP DEFAULT NOW()
    )', 'tenant_' || tenant_id);
    
  -- Add indexes
  EXECUTE format('CREATE INDEX %I ON %I.users (email)', 
    'idx_tenant_' || tenant_id || '_users_email',
    'tenant_' || tenant_id);
END;
$$ LANGUAGE plpgsql;

Step 6: API Integration

import { NextRequest, NextResponse } from 'next/server';
import { tenantDb } from '@/lib/tenant-db';

export async function GET(
  req: NextRequest,
  { params }: { params: { tenantId: string } }
) {
  try {
    const users = await tenantDb.getTenantUsers(params.tenantId);
    return NextResponse.json({ users });
  } catch (error) {
    return NextResponse.json({ error: 'Failed to fetch users' }, { status: 500 });
  }
}

export async function POST(
  req: NextRequest,
  { params }: { params: { tenantId: string } }
) {
  try {
    const { name, email } = await req.json();
    const user = await tenantDb.createTenantUser(params.tenantId, name, email);
    return NextResponse.json({ user }, { status: 201 });
  } catch (error) {
    return NextResponse.json({ error: 'Failed to create user' }, { status: 500 });
  }
}

Summary

PostgreSQL multi-tenancy can be achieved through schema separation for strong isolation or shared tables with RLS for simpler management. Choose based on tenant count, data size, and isolation requirements.


Share this post on:

Previous Post
Automated PostgreSQL Backups and Point-in-Time Recovery
Next Post
Using PostGIS for Location-Based Features in PostgreSQL