Multi-tenancy in Ruby on Rails - many tenants in one application

Ruby on Rails applications sometimes require an ability to supply the same business logic with separation of models data for a group of clients. This circumstance is also known as multi-tenancy. This way one user does not see other users' activities nor has access to their data. The most common example is a blog application where a user publishes articles. He can edit his own articles, but doesn’t have access to other authors' articles. Unfortunately, Ruby on Rails doesn’t have any built-in module which deals with this issue.

The core problem in multi-tenancy is to ensure data scoping and isolation for each client. This can be done on the database or the active model layer. As it's usually the case, each of these solutions has pros and cons.

Multi-tenancy on the database layer

If we decide to add data scoping on the database layer we have to create for each user a new database or schema (when database engine supports them). This gives us the best data separation (avoiding data leaking to another tenant) and maintenance (easy exporting/importing). Unfortunately, some applications have architecture constraints which makes creating tenants directly on the database impossible. For example, only one database can exist in the application or the database engine doesn’t support schemas. Moreover, if we use separate databases (not schemas) for the tenants, the application then uses more physical resources and it’s slower (many connections to the databases).

Multi-tenancy on the model layer

The concept of multi-tenancy on the model layer is based on ActiveRecord default scopes. In one database, each customer is connected with his own tenanted models by the foreign key. Then CRUD operations on models can be assigned to the particular tenant (usage of default_scope). This solution is faster and easier to implement, but also more risky. A bug in the application can cause irreversible data leaks from one tenant to another.

Multi-tenancy implementation

Multi-tenancy in Rails can be handled by gems libraries or be implemented directly into the application. The two most popular Ruby Gems which allow for creating multi-tenancy in Rails are:

Sample application

Let's create two versions of a sample blog Rails-5.0.2v application which will demonstrate how these gems work. We also use Postgresql database to show schema-based tenants.

rails new blog -d postgresql  
bundle exec rake db:create  

We need some model data. Authors will play the role of tenants' customers and posts will be tenanted data.

bundle exec rails g model author name  
bundle exec rails g model post content  
Using Apartment gem

First, we have to add the gem to Gemfile:

gem 'apartment'  

Then generate gem’s configuration file config/initializers/apartment.rb:

bundle exec rails generate apartment:install  

Each author must have a separate schema in Postgres database. Name of the author will be the name of the schema, so we have to add a unique constraint to it (we assume that author's name is a valid Postgres schema name):

The authors table migration file:

class CreateAuthors < ActiveRecord::Migration[5.0]  
  def change
    create_table :authors do |t|
      t.string :name, null: false
    end
    add_index :authors, :name, unique: true
  end
end  

We also add validation in app/models/author.rb:

validates :name, presence: true, uniqueness: true  

Finally, we run migration:

bundle exec rake db:migrate  

In Apartment gem, each tenant should be created and deleted avowedly (in our case when the new author is created and removed) so let’s add Apartment methods calls in app/models/author.rb callbacks:

after_create do |author|  
  Apartment::Tenant.create(author.name)
end

after_destroy do |author|  
  Apartment::Tenant.drop(author.name)
end  

This will create a Postgres schema for new the author and remove from the databsase it when the author will be deleted.

In our simple example Post model will belong to a tenant and the Author will be excluded from it and remain in the global (public) namespace. To ensure that we need to add some configuration in config/initializers/apartment.rb:

config.excluded_models = %w{ Author }  

The apartment gem also needs lists of all tenants names to run migrations for them and create proper tables in Postgresql schemas. In our example, we need to modify only one uncommented option in config/initializers/apartment.rb to:

config.tenant_names = lambda { Author.pluck(:name) }  

Finally, we can add some data in Rails console to see how multi-tenancy in the Apartment gem works.

Let’s create two authors:

Author.create(name: 'Rob')  
Author.create(name: 'David')  

This will create users, but also two schemas with posts table for both of them.

By default, we are in the public schema. In order to switch to a different schema we use a special Apartment method:

Apartment::Tenant.switch!('Rob')  

This will set current apartment to author named Rob (schema for tenant was created after creation of author). Let’s create some posts for this author in this tenant:

Post.create(content: "Rob’s article")  
Post.all => #<ActiveRecord::Relation [#<Post id: 1, content: "Rob article", created_at: "2017-04-02 17:32:39", updated_at: "2017-04-02 17:32:39">]>  

Let’s switch to the second author and show all articles:

Apartment::Tenant.switch!('David')  
Post.all => #<ActiveRecord::Relation []>  

And we get an empty result, so multi-tenancy works!

Moreover, we see all authors:

Author.all => #<ActiveRecord::Relation [#<Author id: 1, name: "Rob">, #<Author id: 2, name: "David">]>  

Because the Author model is excluded from multi-tenancy - it’s stored in the public schema and can be seen globally. The public schema has its own posts table, so author's posts will not be visible in this schema.

Apartment gem has also a built-in ability to switch tenants per HTTP request more - info can be found on Github gem page.

Using actsastenant gem

A different approach to multi-tenancy has actsastenant gem. It uses a default scope mechanism based on foreign keys to connect tenant with the customer.

Let’s start from the point where we have only Author and Post model in our sample Rails application. First, we will add gem to Gemfile and run bundler to install it:

gem 'acts_as_tenant'  

Then, for Post model, we have to add a relation to the Author model:

bundle exec rails g migration add_author_reference_to_posts author:references  
bundle exec rake db:migrate  

For each model that will belong to the tenant we have to add a special declaration which tells actsastenenant gem which model defines the tenant. In our case, we add declaration to app/models/post.rb:

class Post < ApplicationRecord  
  acts_as_tenant(:author)
end  

Let’s create two authors like the last time:

Author.create(name: 'Rob')  
Author.create(name: 'David')  

To set the current tenant we use:

ActsAsTenant.current_tenant = Author.find_by(name: 'Rob')  

Now we can create a post for this tenant:

Post.create(content: "Rob's article")  

When calling:

Post.all => #<Post id: 1, content: "Rob's article", created_at: "2017-04-02 19:43:25", updated_at: "2017-04-02 19:43:25", author_id: 1>  

We get the last post as well we can see that author_id foreign key was set automatically by actsastenant gem. Let’s switch to the second author:

ActsAsTenant.current_tenant = Author.find_by(name: 'David')  
Post.all => #<ActiveRecord::Relation []>  

Now we get no posts because the post was created in the first tenant.

Like in the Apartment gem authors will be seen globally:

Author.all => #<ActiveRecord::Relation [#<Author id: 1, name: "Rob">, #<Author id: 2, name: "David">]>  

In actsastenant gem not setting tenant with ActsAsTenant.current_tenant= will cause foreign key that represents tenant to be nil. In actsastenant gem initialize file config/initializers/acts_as_tenant.rb we can force setting tenant:

ActsAsTenant.configure do |config|  
  config.require_tenant = false # true
end  

What to choose?

As usual, there is no universally best solution for multi-tenancy, but it seems most reasonable to use the Apartment gem when we can use Postgres schemas. Both gems have a built-in ability to automatically set tenants per request based on the subdomain, URL or a custom method in the application controller. These gems are also quite flexible, so creating your own solution for multi-tenancy is in most cases pointless.