Apartment.configure do |config|
config.excluded_models = %w[Tenant User]
config.tenant_names = -> { Tenant.pluck(:schema_name) }
config.use_schemas = true
end
Rails.application.config.middleware.use Apartment::Elevators::Subdomain
class Tenant < ApplicationRecord
after_create :create_schema
validates :schema_name, presence: true, uniqueness: true,
format: { with: /\A[a-z0-9_]+\z/, message: 'only lowercase letters, numbers, and underscores' }
private
def create_schema
Apartment::Tenant.create(schema_name)
end
end
class ApplicationController < ActionController::Base
before_action :set_tenant
private
def set_tenant
tenant_name = request.subdomains.first
if tenant_name.present? && Tenant.exists?(schema_name: tenant_name)
Apartment::Tenant.switch(tenant_name)
else
render json: { error: 'TENANT_NOT_FOUND' }, status: :not_found
end
end
end
SaaS applications serving multiple customers (tenants) need data isolation to prevent cross-tenant data leaks. The apartment gem provides schema-based multi-tenancy for PostgreSQL where each tenant gets a separate schema, ensuring complete database-level isolation. Tenant switching happens via Apartment::Tenant.switch(tenant_name), typically in a middleware or controller concern that extracts the tenant from subdomain or request headers. Migrations run across all tenant schemas automatically. This approach provides strong isolation with reasonable performance overhead. The alternative—row-based tenancy with a tenant_id column—is simpler but requires perfect discipline in all queries to avoid accidental cross-tenant data exposure. Schema-based tenancy makes mistakes obvious.