Sublet
Hierarchy-aware, opt-in multi-tenancy for ActiveRecord.
- Declare a tenant root with
sublet
. - Declare subletters with
subletter(:parent_assoc)
— the parent can be the tenant or another subletter. - You set
Sublet.current_tenant
yourself (only in the controllers you want). - Sublet auto-scopes queries via
default_scope
so admin filters, counts, etc. follow the tenant. - Fails open when tenant chains can't be resolved (no scoping applied).
- Includes automatic validation to prevent tenant mismatches.
Install
Add this line to your application's Gemfile:
gem "sublet", "~> 0.1.0"
And then execute:
bundle install
Or install it yourself as:
gem install sublet
Requirements
- Ruby >= 3.1.0
- ActiveRecord >= 6.1
- ActiveSupport >= 6.1
Usage
Basic Setup
class Company < ApplicationRecord
include Sublet::Model
sublet
end
class Department < ApplicationRecord
include Sublet::Model
subletter(:company)
belongs_to :company
end
class Employee < ApplicationRecord
include Sublet::Model
subletter(:department)
belongs_to :department
has_one :company, through: :department
end
Controller Integration
In a controller where you want tenant scoping:
class Admin::BaseController < ApplicationController
before_action do
Sublet.current_tenant = current_user.company
end
end
Or use the included controller helper:
class Admin::BaseController < ApplicationController
before_action do
set_current_sublet(current_user.company)
end
end
Temporary Tenant Switching
# Temporarily switch to a different tenant
Sublet.with_tenant(Company.find(1)) do
Employee.count # Scoped to Company 1
end
# Temporarily bypass tenant scoping
Sublet.without_tenant do
Employee.unscoped.count # All employees across all tenants
end
Advanced Examples
Multi-level Hierarchy
class Account < ApplicationRecord
include Sublet::Model
sublet
end
class Company < ApplicationRecord
include Sublet::Model
subletter(:account)
belongs_to :account
end
class Department < ApplicationRecord
include Sublet::Model
subletter(:company)
belongs_to :company
end
class Employee < ApplicationRecord
include Sublet::Model
subletter(:department)
belongs_to :department
end
Tenant Without Scoping
class Company < ApplicationRecord
include Sublet::Model
sublet(scope: false) # Declare as tenant but don't auto-scope
end
Validation
Sublet automatically validates that records belong to the current tenant:
# This will fail validation if the employee doesn't belong to current_tenant
employee = Employee.new(department: wrong_department)
employee.valid? # => false
employee.errors[:base] # => ["Tenant mismatch"]
Key Features
- Hierarchy-aware: Supports multi-level tenant hierarchies (Account → Company → Department → Employee)
-
Opt-in: Only applies scoping where you explicitly set
current_tenant
- Fails open: If tenant chain can't be resolved, no scoping is applied (safe default)
- Automatic validation: Prevents tenant mismatches during record creation/updates
-
Controller helpers: Includes
set_current_sublet
helper method -
Temporary switching:
with_tenant
andwithout_tenant
for temporary context changes - STI-safe: Works correctly with Single Table Inheritance
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake spec
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and the created tag, and push the .gem
file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/gogrow-dev/sublet. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
License
The gem is available as open source under the terms of the MIT License.
Notes
- If a subletter chain can't be resolved to a tenant, Sublet fails open (no scoping).
- Tenants themselves are not scoped by default (use
sublet(scope: true)
to enable). - The gem automatically includes controller helpers when Rails is present.