class Current < ActiveSupport::CurrentAttributes
attribute :tenant, :request_id
def tenant=(tenant)
super
Rails.logger.tagged("tenant=#{tenant&.id}") if tenant
end
def tenant_id
tenant&.id
end
def tenant!
tenant || raise(Tenant::NotResolved, "No tenant is set for the current request")
end
end
class ApplicationController < ActionController::Base
class Tenant
NotResolved = Class.new(StandardError)
end
before_action :authenticate_user!
around_action :with_tenant
rescue_from Tenant::NotResolved do
head :not_found
end
private
def with_tenant
set_tenant
yield
ensure
Current.reset
end
def set_tenant
Current.request_id = request.request_id
Current.tenant = current_user&.tenant
Current.tenant! # raises Tenant::NotResolved when nil
end
end
module TenantScoped
extend ActiveSupport::Concern
included do
belongs_to :tenant
default_scope do
if Current.tenant_id
where(tenant_id: Current.tenant_id)
else
all
end
end
before_validation :assign_current_tenant, on: :create
end
class_methods do
def unscoped_across_tenants
unscoped { yield }
end
end
private
def assign_current_tenant
self.tenant_id ||= Current.tenant_id
end
end
class Invoice < ApplicationRecord
include TenantScoped
has_many :line_items, dependent: :destroy
validates :number, presence: true, uniqueness: { scope: :tenant_id }
scope :overdue, -> { where("due_on < ?", Date.current).where(paid_at: nil) }
def self.monthly_totals_all_tenants
unscoped_across_tenants do
group(:tenant_id).group_by_month(:issued_on).sum(:amount_cents)
end
end
end
Multi-tenant Rails apps that share one database run a constant risk: a query that forgets to filter by tenant leaks one customer's data into another's response. This snippet centralizes that filtering so individual controllers and models never have to remember it, using ActiveSupport::CurrentAttributes to hold the active tenant for the duration of a request and a default_scope to bind every query to it.
In Current the tenant lives in a request-scoped attribute. CurrentAttributes is per-thread and, critically, is reset automatically between requests by Rails' executor, so a tenant set on one request can never bleed into the next even under a threaded server like Puma. The tenant= writer also mirrors the id into ActiveSupport::Notifications tags, which makes the tenant show up in structured logs for free.
ApplicationController resolves the tenant from the authenticated user and wraps the whole action in an around_action. Using around_action rather than before_action matters: the ensure block guarantees Current.reset runs even when the action raises, so a failure mid-request cannot leave a stale tenant set for the next unit of work on that thread. set_tenant raises Tenant::NotResolved when no tenant can be determined, turning a silent data-leak bug into a loud 404.
TenantScoped is a concern mixed into every tenant-owned model. Its default_scope reads Current.tenant_id so ordinary finds like Invoice.find(id) are automatically constrained. A before_validation stamps tenant_id on new records so writes can't be misattributed. The unscoped_across_tenants helper is the deliberate escape hatch for admin tools and background jobs, forcing that dangerous operation to be explicit and greppable.
The trade-offs are real: default_scope is famously sticky and can surprise developers who expect unscoped to behave a certain way, and background jobs must set Current.tenant themselves since they run outside the request cycle. But for request-driven code the pattern removes an entire class of authorization mistakes by making the safe path the default one. Reaching for this approach makes sense once tenant checks start appearing copy-pasted across many controllers.
Related snips
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
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
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
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
Share this code
Here's the card — post it anywhere.