ruby
41 lines · 8 steps
How a multitenant concern scopes Rails models
A concern that automatically confines every query, insert, and validation to the current tenant's account.
Explained by
highlit
1module Multitenant
2 extend ActiveSupport::Concern
3
4 included do
5 belongs_to :account, inverse_of: false
6
7 default_scope do
8 if Current.account
9 where(account_id: Current.account.id)
10 else
11 raise Multitenant::MissingTenantError, "#{name} queried without a current account"
12 end
13 end
14
15 before_validation :assign_current_account, on: :create
16
17 validate :account_matches_current_tenant
18 end
19
20 class_methods do
21 def unscoped_across_tenants
22 unscope(where: :account_id)
23 end
24 end
25
26 private
27
28 def assign_current_account
29 self.account ||= Current.account
30 end
31
32 def account_matches_current_tenant
33 return if account_id.blank? || Current.account.nil?
34
35 unless account_id == Current.account.id
36 errors.add(:account, "does not match the current tenant")
37 end
38 end
39end
40
41class Multitenant::MissingTenantError < StandardError; end
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A default scope tied to a request-scoped Current attribute keeps every model query confined to one tenant without touching call sites.
- 2Failing loudly when no tenant is set prevents accidental cross-tenant data leaks better than silently returning everything.
- 3Assigning and validating the tenant on save closes the write side of the isolation the default scope enforces on reads.
Related explainers
ruby
require "shellwords" require "open3" module Backup
Building safe shell commands in Ruby
shell-out
subprocess
command-injection
Intermediate
7 steps
ruby
class UserAgentParser BROWSERS = [ [/Edg\/([\d.]+)/, "Edge"], [/OPR\/([\d.]+)/, "Opera"],
Parsing user-agent strings in Ruby
regex
pattern-matching
lookup-tables
Intermediate
8 steps
ruby
class LogAggregator BUCKET_FORMAT = "%Y-%m-%dT%H:%M" def initialize(entries)
Bucketing log entries by the minute in Ruby
aggregation
hashing
enumerable
Intermediate
5 steps
ruby
class WeeklySignupsReport DEFAULT_WEEKS = 12 def initialize(weeks: DEFAULT_WEEKS, source: User.all)
Building a weekly signups report in Rails
service object
aggregation
group by
Intermediate
7 steps
ruby
class ApplicationController < ActionController::Base EXPERIMENTS = { checkout_button_color: %w[control blue green], onboarding_flow: %w[control streamlined]
How A/B test cohorts are assigned in Rails
a-b-testing
cookies
hashing
Intermediate
8 steps
ruby
class SnippetHighlighter CONTEXT_RADIUS = 60 MAX_TERMS = 8
Building search-result snippets in Ruby
regular-expressions
text-processing
search
Intermediate
9 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/how-a-multitenant-concern-scopes-rails-models-explained-ruby-44f6/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.