A Rails gem that provides PostgreSQL Row Level Security (RLS) based multi-tenancy for Rails applications.
π Learn more about PostgreSQL Row Level Security: PostgreSQL RLS Documentation
This gem relies on PostgreSQL Row-Level Security (RLS) for tenant isolation. You MUST create a dedicated database role with proper RLS permissions before using this gem in production.
- π Row Level Security: Automatic tenant isolation using PostgreSQL RLS
- π‘οΈ Security Validation: Prevents running with privileged database users
- π Context Switching: Easy tenant context management
- π¦ Auto-inclusion: Automatic model configuration
- π Generators: Rails generators for quick setup
- βοΈ Configurable: Flexible configuration options
- π Subdomain Middleware: Automatic tenant switching based on subdomain
Create a dedicated role for your Rails application:
CREATE ROLE app_user
WITH LOGIN
CREATEDB -- can create databases
CREATEROLE -- can create/modify other roles (except superuser)
NOINHERIT -- does not inherit privileges from roles it belongs to
NOREPLICATION -- cannot use replication
NOBYPASSRLS -- cannot bypass Row-Level Security
NOSUPERUSER -- is not a superuser
PASSWORD 'strong_password';If you don't have a database created, you can create one with the new role:
CREATE DATABASE your_db_name OWNER app_user;If you already have a database created, make sure to grant ownership of the database to the new role:
ALTER DATABASE your_db_name OWNER TO app_user;NOBYPASSRLS: ESSENTIAL for RLS security - prevents bypassing Row-Level Security policies that enforce tenant isolationNOSUPERUSER: Prevents superuser privileges that could compromise RLS policiesLOGIN: Allows the role to connect to the databaseCREATEDB: Enables database creation for development/testing environmentsCREATEROLE: Allows creating other roles for application-specific usersNOINHERIT: Ensures the role does not inherit privileges from parent rolesNOREPLICATION: Prevents the role from being used for replication (security)
Without NOBYPASSRLS, Row-Level Security policies can be bypassed, completely breaking tenant isolation and exposing data across tenants.
Update your config/database.yml to use the new role:
Add this line to your application's Gemfile:
gem 'rls_multi_tenant'And then execute:
bundle install-
Install the gem configuration:
rails generate rls_multi_tenant:install
-
Configure the gem settings: Edit
config/initializers/rls_multi_tenant.rbto customize your tenant model:RlsMultiTenant.configure do |config| config.tenant_class_name = "Tenant" # Your tenant model class (e.g., "Organization", "Company") config.tenant_id_column = :tenant_id # Tenant ID column name config.enable_security_validation = true # Enable security checks (prevents running with superuser privileges) config.enable_subdomain_middleware = true # Enable subdomain-based tenant switching (default: true) config.subdomain_field = :subdomain # Field to use for subdomain matching (default: :subdomain) config.excluded_subdomains = ['www'] # Subdomains to exclude from tenant lookup (default: ['www']) end
-
Setup the tenant model and migrations:
rails generate rls_multi_tenant:setup
-
Run migrations:
rails db:migrate
Create a new model:
rails generate rls_multi_tenant:model User name emailYour models automatically include the MultiTenant concern:
class User < ApplicationRecord
# Automatically includes MultiTenant concern
include RlsMultiTenant::Concerns::MultiTenant
end# Create a new tenant with subdomain
tenant = Tenant.create!(name: "Company A", subdomain: "company-a")# Switch tenant context for a block
Tenant.switch(tenant) do
User.create!(name: "User from Company A", email: "[email protected]") # Automatically assigned to current tenant
end
# Switch tenant context permanently
Tenant.switch!(tenant)
User.create!(name: "User from Company A", email: "[email protected]")
Tenant.reset! # Reset context
# Get current tenant
current_tenant = Tenant.currentThe gem includes middleware that automatically switches tenants based on the request subdomain. This is enabled by default and works seamlessly with your tenant model.
The middleware automatically:
- Extracts the subdomain from the request host
- Finds the matching tenant by the subdomain field
- Switches the tenant context for the duration of the request
- Resets the context after the request completes
Usage:
# Create tenants with subdomains
tenant1 = Tenant.create!(name: "Company A", subdomain: "company-a")
tenant2 = Tenant.create!(name: "Company B", subdomain: "company-b")
# Users visiting company-a.yourdomain.com will automatically be in tenant1's context
# Users visiting company-b.yourdomain.com will automatically be in tenant2's context
# Users visiting yourdomain.com (no subdomain) will have no tenant contextExcluded Subdomains:
You can configure subdomains that should be excluded from tenant lookup and treated as public access (no tenant context). By default, www is excluded.
RlsMultiTenant.configure do |config|
# Exclude multiple subdomains from tenant lookup
config.excluded_subdomains = ['www', 'admin', 'api']
endWhen a request comes from an excluded subdomain (e.g., www.yourdomain.com), the middleware will:
- Skip tenant lookup
- Treat the request as public access (no tenant context)
- Not raise an error for missing tenant
This is useful for:
- Main website access (
www) - Admin panels that shouldn't be tenant-scoped
- API endpoints that need public access
Models that don't include RlsMultiTenant::Concerns::TenantContext are automatically treated as public models and can be accessed without tenant context. This provides a secure, explicit way to separate tenant-specific and public models.
Example:
# Public models (no tenant association)
class PublicPost < ApplicationRecord
# No TenantContext concern included
# These models are accessible without tenant context
end
# Tenant-specific models (automatically generated)
class User < ApplicationRecord
# Automatically includes MultiTenant concern
# These models require tenant context and are constrained by RLS
include RlsMultiTenant::Concerns::MultiTenant
endSecurity Benefits:
- Explicit Intent: Models must explicitly include
TenantContextto be tenant-constrained - Fail-Safe: Public models are clearly separated from tenant models
- No Configuration Drift: Can't accidentally expose tenant data through misconfiguration
- Rails 6.0+
- PostgreSQL 9.5+ (with UUID extension support)
- Ruby 2.7+
This gem uses UUIDs for the tenant model by default to ensure proper multi-tenant isolation. The enable_uuid migration must be run before creating tenant tables.
- Fork the repository
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a Pull Request
The gem is available as open source under the terms of the MIT License.