ruby 87 lines · 4 tabs

Tenant Isolation in Rails with CurrentAttributes and an around_action

Shared by codesnips Jul 2026
4 tabs
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
4 files · ruby Explain with highlit

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

Share this code

Here's the card — post it anywhere.

Tenant Isolation in Rails with CurrentAttributes and an around_action — share card
Link copied