PgMultitenantSchemas
A modern Ruby gem that provides PostgreSQL schema-based multitenancy with automatic tenant resolution and schema switching. Compatible with Rails 7+ and Ruby 3.0+, with optimizations for Rails 8, focusing on security, performance, and developer experience.
โจ Features
- ๐ข Schema-based multitenancy - Complete tenant isolation using PostgreSQL schemas
- ๐ Automatic schema switching - Seamlessly switch between tenant schemas
- ๐ Subdomain resolution - Extract tenant from request subdomains
- ๐ Rails 7+ compatible - Works with Rails 7 and optimized for Rails 8
- ๏ฟฝ๏ธ Security-first design - Database-level tenant isolation
- ๐งต Thread-safe - Safe for concurrent operations
- ๐ Comprehensive logging - Track schema operations
- โก High performance - Minimal overhead with clean API
๐ Requirements
Requirements
- Ruby 3.0+
- Rails 7.0+
- PostgreSQL 12+
- pg gem: 1.5 or higher
Installation
Add this line to your application's Gemfile:
gem 'pg_multitenant_schemas'
And then execute:
bundle install
Or install it yourself as:
gem install pg_multitenant_schemas
Configuration
Configure the gem in your Rails initializer (config/initializers/pg_multitenant_schemas.rb
):
PgMultitenantSchemas.configure do |config|
config.connection_class = 'ApplicationRecord' # or 'ActiveRecord::Base'
config.tenant_model_class = 'Tenant' # your tenant model
config.default_schema = 'public'
config.excluded_subdomains = ['www', 'api', 'admin']
config.development_fallback = true # for development
config.auto_create_schemas = true # automatically create missing schemas
end
Usage
Migration Management
The gem provides automated migration management across all tenant schemas:
Running Migrations
# Migrate all tenant schemas at once (recommended)
rails tenants:migrate
# Migrate a specific tenant
rails tenants:migrate_tenant[acme_corp]
# Check migration status across all tenants
rails tenants:status
Setting Up New Tenants
# Create new tenant with full setup (schema + migrations)
rails tenants:create[new_tenant]
# Create tenant with attributes using JSON
rails tenants:new['{"subdomain":"acme","name":"ACME Corp","domain":"acme.com"}']
# Setup schemas for all existing tenants (migration from single-tenant)
rails tenants:setup
Migration Workflow
-
Create your migration as usual:
rails generate migration AddEmailToUsers email:string
-
Deploy to all tenants with a single command:
rails tenants:migrate
-
Check status to verify migrations across tenants:
rails tenants:status
This shows detailed migration status:
๐ Migration Status Report โธ acme_corp: โ Up to date (5 migrations) โธ beta_corp: โ ๏ธ 2 pending migrations โธ demo_corp: โ Up to date (5 migrations) ๐ Overall: 2/3 tenants current, 2 migrations pending
The migrator automatically:
- Runs migrations across all tenant schemas
- Provides detailed progress feedback
- Handles errors gracefully per tenant
- Maintains migration version tracking per schema
- Supports rollbacks for individual tenants
Advanced Migration Operations
# List all tenant schemas
rails tenants:list
# Rollback specific tenant
rails tenants:rollback[tenant_name,2] # rollback 2 steps
# Drop tenant schema (DANGEROUS - requires confirmation)
rails tenants:drop[old_tenant]
Programmatic Access
# Use the Migrator directly in your code
PgMultitenantSchemas::Migrator.migrate_all
PgMultitenantSchemas::Migrator.setup_tenant('new_client')
PgMultitenantSchemas::Migrator.migration_status
Tenant Resolution
# Extract tenant from subdomain
subdomain = PgMultitenantSchemas.extract_subdomain('acme.myapp.com')
# => 'acme'
# Find tenant by subdomain
tenant = PgMultitenantSchemas.find_tenant_by_subdomain('acme')
# Resolve tenant from Rails request
tenant = PgMultitenantSchemas.resolve_tenant_from_request(request)
Context Management
# Switch to tenant context
PgMultitenantSchemas.switch_to_tenant(tenant)
# Use block-based context switching
PgMultitenantSchemas.with_tenant(tenant) do
# All queries here use tenant's schema
User.all # Queries tenant_123.users
end
# Check current context
PgMultitenantSchemas.current_tenant
PgMultitenantSchemas.current_schema
Rails Integration
In your ApplicationController:
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
In your models:
class Tenant < ApplicationRecord
include PgMultitenantSchemas::Rails::ModelConcern
after_create :create_tenant_schema
after_destroy :drop_tenant_schema
end
๐๏ธ Multi-Tenant Architecture Principles
Core Principle: Tenant Isolation
In well-designed multi-tenant applications, tenants should NOT communicate with each other directly. Each tenant operates in complete isolation for security, compliance, and data integrity.
# โ
GOOD: Isolated tenant operations
PgMultitenantSchemas.with_tenant(tenant) do
# All operations are isolated to this tenant's schema
User.create!(name: "John", email: "john@example.com")
Order.where(status: "pending").update_all(status: "processed")
end
# โ BAD: Cross-tenant data sharing (security risk!)
# Never do: tenant_a.users.merge(tenant_b.users)
When Cross-Schema Operations Are Appropriate
There are only 3 legitimate use cases for cross-schema operations:
1. Platform Analytics & Reporting (Admin-only)
# Platform owner needs aggregate statistics across all tenants
def platform_analytics
PgMultitenantSchemas::SchemaSwitcher.with_connection do |conn|
conn.execute(<<~SQL)
SELECT
schemaname as tenant,
COUNT(*) as total_users,
SUM(revenue) as total_revenue
FROM (
SELECT 'tenant_a' as schemaname, COUNT(*) as users,
(SELECT SUM(amount) FROM tenant_a.orders) as revenue
UNION ALL
SELECT 'tenant_b' as schemaname, COUNT(*) as users,
(SELECT SUM(amount) FROM tenant_b.orders) as revenue
) stats
GROUP BY schemaname;
SQL
end
end
2. Tenant Migration & Data Operations (Admin-only)
# Moving data between environments or consolidating tenants
def migrate_tenant_data(from_tenant, to_tenant)
PgMultitenantSchemas.with_tenant(from_tenant) do
users = User.all.to_a
end
PgMultitenantSchemas.with_tenant(to_tenant) do
users.each { |user| User.create!(user.attributes.except('id')) }
end
end
3. Shared Reference Data (Read-only)
# Shared lookup tables that all tenants can read (e.g., countries, currencies)
class Country < ApplicationRecord
self.table_name = 'public.countries' # Shared across all schemas
end
# In tenant context, can still access shared data
PgMultitenantSchemas.with_tenant(tenant) do
user = User.create!(name: "John", country: Country.find_by(code: 'US'))
end
๐ซ Anti-Patterns to Avoid
# โ NEVER: Direct tenant-to-tenant communication
def share_data_between_tenants(tenant_a, tenant_b)
# This violates tenant isolation!
end
# โ NEVER: Cross-tenant user authentication
def authenticate_user_across_tenants(email)
# Users should only exist in their tenant's schema
end
# โ NEVER: Cross-tenant business logic
def process_order_with_other_tenant_data(order_id, other_tenant)
# Business logic should be isolated per tenant
end
Why Tenant Isolation Matters
- Security: Prevents accidental data leaks between customers
- Compliance: GDPR, HIPAA, SOX require strict data separation
- Performance: Each tenant's queries are optimized for their data size
- Reliability: One tenant's issues don't affect others
- Scalability: Easy to move tenants to different database servers
Architecture Recommendation
# โ
Proper multi-tenant architecture
class ApplicationController < ActionController::Base
include PgMultitenantSchemas::Rails::ControllerConcern
before_action :authenticate_user!
before_action :resolve_tenant
before_action :ensure_user_belongs_to_tenant
private
def resolve_tenant
@tenant = resolve_tenant_from_subdomain
switch_to_tenant(@tenant) if @tenant
end
def ensure_user_belongs_to_tenant
# Ensure current user can only access their tenant's data
redirect_to_login unless current_user.tenant == @tenant
end
end
Bottom Line: Cross-tenant operations should be extremely rare, admin-only, and carefully audited. The vast majority of your application should operate within a single tenant's schema.
๐ Documentation
Complete Architecture Documentation
Detailed documentation for each core component:
- ๐ Core Architecture Overview - Complete system architecture
- ๐ง Schema Switcher - Low-level PostgreSQL schema operations
- ๐งต Context Management - Thread-safe tenant context handling
- ๐ Migration System - Automated migration management
- โ๏ธ Configuration - Gem settings and customization
- ๐ Tenant Resolver - Tenant identification strategies
- ๐ค๏ธ Rails Integration - Framework components and patterns
- ๐จ Error Handling - Exception classes and error management
Examples and Patterns
- Schema Operations - Core schema management
- Context Management - Thread-safe tenant switching
- Migration Workflow - Automated migration examples
- Rails Controllers - Framework integration patterns
๐งช Testing
This gem includes a comprehensive test suite with both unit and integration tests.
Running Tests
# Run unit tests only (fast, no database required)
bundle exec rspec
# Run integration tests (requires PostgreSQL)
bundle exec rspec --tag integration
# Run all tests
bundle exec rspec --no-tag
Test Categories
- Unit Tests (65 examples): Fast, isolated component testing
- Integration Tests (21 examples): Real PostgreSQL multi-schema operations
- Performance Tests: Memory usage and thread safety validation
- Edge Cases: Error handling and boundary condition testing
See Testing Guide for detailed information about the test suite and Integration Testing Guide for PostgreSQL integration testing details.
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake spec
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and the created tag, and push the .gem
file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/pg_multitenant_schemas. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the PgMultitenantSchemas project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.