PostgreSQL Schema-Based Multitenancy

Modern Rails gem for secure, isolated, high-performance multi-tenant applications

Powerful Features

๐Ÿข

Schema Isolation

Each tenant gets their own PostgreSQL schema. Complete data isolation with shared application code.

๐Ÿ”„

Automatic Switching

Seamlessly switch between tenant schemas. Context is maintained throughout the request lifecycle.

๐ŸŒ

Subdomain Resolution

Extract tenant from request subdomains. Built-in tenant identification strategies.

๐Ÿงต

Thread-Safe

Safe for concurrent operations. Proper context isolation for multi-threaded environments.

โšก

High Performance

Minimal overhead. Optimized for Rails 8+ with efficient schema switching.

๐Ÿ”’

Security-First

Database-level tenant isolation. No risk of accidental cross-tenant queries.

Installation

Requirements

Setup

Add to your Gemfile:

gem 'pg_multitenant_schemas'
bundle install

Configuration

Create config/initializers/pg_multitenant_schemas.rb:

PgMultitenantSchemas.configure do |config|
  config.connection_class = 'ApplicationRecord'
  config.tenant_model_class = 'Tenant'
  config.default_schema = 'public'
  config.excluded_subdomains = ['www', 'api', 'admin']
  config.development_fallback = true
  config.auto_create_schemas = true
end

Quick Start

Migrate All Tenants

# Migrate all tenant schemas at once
rails tenants:migrate

# Migrate a specific tenant
rails tenants:migrate_tenant[acme_corp]

# Check migration status
rails tenants:status

Create New Tenant

# Create tenant with setup (schema + migrations)
rails tenants:create[new_tenant]

# Create with attributes
rails tenants:new['{"subdomain":"acme","name":"ACME Corp"}']

Context Management

// Switch to tenant context
PgMultitenantSchemas.switch_to_tenant(tenant)

# Use with block
PgMultitenantSchemas.with_tenant(tenant) do
  User.all  # Queries tenant's schema
end

# Get current context
PgMultitenantSchemas.current_schema  #=> "tenant_123"

Rails Integration

class ApplicationController < ActionController::Base
  include PgMultitenantSchemas::Rails::ControllerConcern
  
  before_action :resolve_tenant
  
  private
  
  def resolve_tenant
    tenant = resolve_tenant_from_subdomain
    switch_to_tenant(tenant) if tenant
  end
end

API Reference

Context Class

current_schema

PgMultitenantSchemas::Context.current_schema โ†’ String

Get the current tenant schema name. Returns default schema if not set.

Returns
String The current schema name
PgMultitenantSchemas::Context.current_schema
#=> "tenant_123"

current_tenant

PgMultitenantSchemas::Context.current_tenant โ†’ Object

Get the current tenant object.

PgMultitenantSchemas::Context.current_tenant
#=> #<Tenant id=1, subdomain="acme">

switch_to_tenant(tenant)

PgMultitenantSchemas::Context.switch_to_tenant(tenant) โ†’ void

Switch to the given tenant's schema.

Parameters
tenant Tenant | String Tenant object or schema name
PgMultitenantSchemas::Context.switch_to_tenant(tenant)
# Now all queries use tenant's schema

with_tenant(tenant) { block }

PgMultitenantSchemas::Context.with_tenant(tenant) { |block| } โ†’ Object

Execute block within tenant context. Restores previous context after block.

Parameters
tenant Tenant | String Tenant object or schema name
PgMultitenantSchemas::Context.with_tenant(tenant) do
  User.all  # Queries tenant's schema
  Order.create!(...)
end
# Context restored here

SchemaSwitcher Class

create_schema(schema_name)

PgMultitenantSchemas::SchemaSwitcher.create_schema(schema_name) โ†’ void

Create a new PostgreSQL schema.

Parameters
schema_name String Name of the schema to create

drop_schema(schema_name, cascade: true)

PgMultitenantSchemas::SchemaSwitcher.drop_schema(schema_name, cascade: true) โ†’ void

Drop a PostgreSQL schema.

Parameters
schema_name String Name of the schema to drop
cascade Boolean Drop dependent objects (default: true)

schema_exists?(schema_name)

PgMultitenantSchemas::SchemaSwitcher.schema_exists?(schema_name) โ†’ Boolean

Check if a schema exists in the database.

Parameters
schema_name String Name of the schema to check
if PgMultitenantSchemas::SchemaSwitcher.schema_exists?('tenant_123')
  # Schema exists
end

list_schemas

PgMultitenantSchemas::SchemaSwitcher.list_schemas โ†’ Array[String]

List all schemas in the database.

PgMultitenantSchemas::SchemaSwitcher.list_schemas
#=> ["public", "tenant_123", "tenant_456"]

Migrator Class

migrate_all

PgMultitenantSchemas::Migrator.migrate_all โ†’ Hash

Run migrations across all tenant schemas.

results = PgMultitenantSchemas::Migrator.migrate_all
#=> {
#     success: ["tenant_1", "tenant_2"],
#     failed: [],
#     total_migrations: 5
#   }

setup_tenant(schema_name)

PgMultitenantSchemas::Migrator.setup_tenant(schema_name) โ†’ Boolean

Create schema and run migrations for a new tenant.

Parameters
schema_name String Name of the tenant schema

migration_status

PgMultitenantSchemas::Migrator.migration_status โ†’ Hash

Get migration status for all tenants.

PgMultitenantSchemas::Migrator.migration_status
#=> {
#     "tenant_1" => { status: :up_to_date, pending: 0 },
#     "tenant_2" => { status: :pending, pending: 2 }
#   }

Common Patterns

Subdomain-Based Routing

class ApplicationController < ActionController::Base
  include PgMultitenantSchemas::Rails::ControllerConcern
  
  before_action :authenticate_user!
  before_action :resolve_tenant_from_subdomain
  before_action :ensure_user_in_tenant
  
  private
  
  def resolve_tenant_from_subdomain
    @tenant = resolve_tenant_from_request(request)
    switch_to_tenant(@tenant) if @tenant
  end
  
  def ensure_user_in_tenant
    redirect_unless current_user.tenant == @tenant
  end
end

Background Jobs with Tenant Context

class ProcessOrderJob
  include Sidekiq::Worker
  
  def perform(order_id, tenant_id)
    tenant = Tenant.find(tenant_id)
    
    PgMultitenantSchemas.with_tenant(tenant) do
      order = Order.find(order_id)
      # Process order in tenant's schema
      order.process!
    end
  end
end

# Enqueue with tenant context
tenant = Tenant.first
ProcessOrderJob.perform_async(order.id, tenant.id)

Cross-Tenant Admin Operations

class AdminController < ApplicationController
  def platform_stats
    stats = {}
    
    Tenant.find_each do |tenant|
      PgMultitenantSchemas.with_tenant(tenant) do
        stats[tenant.subdomain] = {
          users_count: User.count,
          orders_count: Order.count,
          revenue: Order.sum(:amount)
        }
      end
    end
    
    render json: stats
  end
end

Testing with Tenants

RSpec.describe User, type: :model do
  let(:tenant) { create(:tenant) }
  
  before { PgMultitenantSchemas.switch_to_tenant(tenant) }
  after { PgMultitenantSchemas.reset! }
  
  it 'creates user in tenant schema' do
    user = create(:user)
    
    PgMultitenantSchemas.with_tenant(tenant) do
      expect(User.find(user.id)).to eq(user)
    end
  end
end

Complete Documentation

Core Components

Component Purpose Link
Context Thread-safe tenant context management Documentation
SchemaSwitcher Low-level PostgreSQL schema operations Documentation
Migrator Automated migration management Documentation
TenantResolver Tenant identification strategies Documentation
Configuration Gem configuration and settings Documentation
Rails Integration Framework components and patterns Documentation

Guides

Troubleshooting

Production Ready

217
Test Examples
100%
Tests Passing
0
CVE Issues
Rails 8
Optimized