Modern Rails gem for secure, isolated, high-performance multi-tenant applications
Each tenant gets their own PostgreSQL schema. Complete data isolation with shared application code.
Seamlessly switch between tenant schemas. Context is maintained throughout the request lifecycle.
Extract tenant from request subdomains. Built-in tenant identification strategies.
Safe for concurrent operations. Proper context isolation for multi-threaded environments.
Minimal overhead. Optimized for Rails 8+ with efficient schema switching.
Database-level tenant isolation. No risk of accidental cross-tenant queries.
Add to your Gemfile:
gem 'pg_multitenant_schemas'
bundle install
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
# 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 tenant with setup (schema + migrations)
rails tenants:create[new_tenant]
# Create with attributes
rails tenants:new['{"subdomain":"acme","name":"ACME Corp"}']
// 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"
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
Get the current tenant schema name. Returns default schema if not set.
PgMultitenantSchemas::Context.current_schema
#=> "tenant_123"
Get the current tenant object.
PgMultitenantSchemas::Context.current_tenant
#=> #<Tenant id=1, subdomain="acme">
Switch to the given tenant's schema.
PgMultitenantSchemas::Context.switch_to_tenant(tenant)
# Now all queries use tenant's schema
Execute block within tenant context. Restores previous context after block.
PgMultitenantSchemas::Context.with_tenant(tenant) do
User.all # Queries tenant's schema
Order.create!(...)
end
# Context restored here
Create a new PostgreSQL schema.
Drop a PostgreSQL schema.
Check if a schema exists in the database.
if PgMultitenantSchemas::SchemaSwitcher.schema_exists?('tenant_123')
# Schema exists
end
List all schemas in the database.
PgMultitenantSchemas::SchemaSwitcher.list_schemas
#=> ["public", "tenant_123", "tenant_456"]
Run migrations across all tenant schemas.
results = PgMultitenantSchemas::Migrator.migrate_all
#=> {
# success: ["tenant_1", "tenant_2"],
# failed: [],
# total_migrations: 5
# }
Create schema and run migrations for a new tenant.
Get migration status for all tenants.
PgMultitenantSchemas::Migrator.migration_status
#=> {
# "tenant_1" => { status: :up_to_date, pending: 0 },
# "tenant_2" => { status: :pending, pending: 2 }
# }
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
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)
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
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
| 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 |