/rails-multitenant

Primary LanguageRubyMIT LicenseMIT

RailsMultitenant

Gem Version Build Status

rails_multitenant is a gem for isolating ActiveRecord models from different tenants. The gem assumes tables storing multi-tenant models include an appropriate tenant id column.

Installation

Add this line to your application's Gemfile:

gem 'rails_multitenant'

And then execute:

$ bundle

Or install it yourself as:

$ gem install rails_multitenant

If you're using Rails, there's nothing else you need to do.

Otherwise, you need to insert RailsMultitenant::Middleware::IsolatedContextRegistry into your middleware stack

Usage

The gem supports two multi-tenancy strategies:

  1. Based on a model attribute, typically a foreign key to an entity owned by another service
  2. Based on a model association

The gem uses ActiveRecord default scopes to make isolating tenants fairly transparent.

Multi-tenancy Based on Model Attributes

The following model is multi-tenant based on an organization_id attribute:

class Product < ActiveRecord::Base
  include RailsMultitenant::MultitenantModel

  multitenant_on :organization_id
end

The model can then be used as follows:

RailsMultitenant::GlobalContextRegistry[:organization_id] = 'my-org'

# Only returns products from 'my-org'
Product.all

# Returns products across all orgs
Product.strip_organization_scope.all

# Or set the current organization in block form
RailsMultitenant::GlobalContextRegistry.with_isolated_registry(organization_id: 'my-org') do
  # Only returns products from 'my-org'
  Product.all
end

By default this adds an ActiveRecord validation to ensure the multi-tenant attribute is present but this can be disabled by passing required: false to multitenant_on.

Multi-tenancy Based on Associated Models

The following model is multi-tenant based on an Organization model:

class Product < ActiveRecord::Base
  include RailsMultitenant::MultitenantModel

  multitenant_on_model :organization
end

The model can then be used as follows:

Organization.current_id = 1

# Only returns products from organization 1
Product.all

# Use the automatically generated belongs_to association to get
# a product's organization
Product.first.organization

# Or set the current organization in block form
Organization.as_current_id(1) do
  # Only returns products from organization 1
  Product.all
end

By default this adds an ActiveRecord validation to ensure the tenant model is present but this can be disabled by passing required: false to multitenant_on_model.

current Models

Classes can be enabled to have current, thread-local instances. For a standard class this is done with:

class MyClass
  include RailsMultitenant::GlobalContextRegistry::Current
end

MyClass.current = MyClass.new

For an ActiveRecord model you can use the following, which additionally allows storing the current model ID.

class MyClass
  include RailsMultitenant::GlobalContextRegistry::CurrentInstance
end

MyClass.current_id = 123
MyClass.current # => #<MyClass id: 123>

Dependency Tracking

For classes that are dependent on other Current classes you can register dependencies.

class DependentClass
  include RailsMultitenant::GlobalContextRegistry::Current
  provide_default :default
  global_context_dependent_on MyClass

  def self.default
    new(MyClass.current.dependent_id)
  end

  def initialize(id)
    @id = id
  end
end

When doing so, clearing the current class on the referenced class will also clear the current context of the dependent class.

klass = MyClass.new
MyClass.current = klass
DependentClass.current # => #<DependentClass id: klass.dependent_id>

MyClass.current = nil
DependentClass.current # => nil

For bi-directional dependencies you can use #global_context_mutually_dependent_on instead of #global_context_dependent_on.

Shorthand

When using rails-multitenant in a project, it is common to need to set values in RailsMultitenant::GlobalContextRegistry at the rails console.

This is difficult to type. Alternatively you can shorten it to RailsMultitenant. For example you might type RailsMultitenant[:organization_id] = 'some value' and it will have the same effect as the long version.

This is mainly intended as a console convenience. Using the long form in source code is fine, and more explicit.

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake false 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 tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/salsify/rails-multitenant. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.