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.
Related snips
-- Simple function
CREATE OR REPLACE FUNCTION get_full_name(
first_name VARCHAR,
last_name VARCHAR
)
RETURNS VARCHAR AS $$
Stored procedures and functions in PostgreSQL
class CommentsController < ApplicationController
before_action :set_post
def create
@comment = @post.comments.build(comment_params)
System test: asserting Turbo Stream responses
class Post < ApplicationRecord
belongs_to :author, class_name: 'User'
has_many :comments, dependent: :destroy
scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
scope :draft, -> { where(published_at: nil) }
ActiveRecord scopes for reusable query logic
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
class PostsController < ApplicationController
def index
@posts = Post.includes(:author)
.order(created_at: :desc)
.page(params[:page])
.per(10)
Turbo Frames: infinite scroll with lazy-loading frame
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
Share this code
Here's the card — post it anywhere.