Ruby Magic

Building a Multi-tenant Ruby on Rails App With Subdomains

Paweł Paweł Dąbrowski on

According to a definition of multitenancy, when an app serves multiple tenants, it means that there are a few groups of users who share common access to the software instance. An excellent example of an app that supports multitenancy is the Jira platform, where each company has its subdomain to access the software, for example, mycompany.atlassian.net.

In this article, I’m going to familiarize you with both theoretical and practical aspects of multitenancy. After discussing a few popular types of approaches to implement support for multiple tenants in your application, I will show you how you can implement two of them in your Rails application. We will build together a simple app with multiple tenants where each tenant will have its subdomain.

After reading this article, you will be able to:

A base knowledge about Rails is required to follow this article and get most of its benefits.

Multitenancy in Theory

As I mentioned before, we can say that the app supports multitenancy when it serves a few groups of users who share common access to the application’s features. A good example of such an app is a blogging platform where every user gets a separate subdomain in the main domain. Each blog has its subdomain but shares the same features as other blogs: comments, articles, and administration dashboard.

Though it might look like multi-tenant apps work similarly on the surface, they can implement different types of multitenancy architecture under the hood. Let’s take a look at the most popular and efficient types of them.

Database-row Level

If you don’t want to use multiple databases in your application, you can select an approach that operates on one central database. Each table in the database consists of the tenant_id column used in every query performed inside the application to pull data that belongs to the given tenant.

The tenant’s setup time in such structure is fast, there are no additional costs, and implementation of this variant is possible everywhere. However, we should be careful as we can lead to a data leak if we forget to include the tenant’s id in the query. Database structure modification would be difficult to maintain for given groups of users, so we should have this disadvantage in mind.

Schema Level

You can still have one central database but with separate tables and schema for each tenant. In contrast to the database-row level approach, you have to change the search path to the tables when switching between tenants instead of modifying the query. In PostgreSQL, you can use the set search_path statement, and in MySQL use statement to switch between schemas.

Before selecting this approach, ensure that it’s possible to use the mentioned statements on your DB server. Keep also in mind that adding new tenants is relatively slow as you have to create a new schema and create tables in the database.

Database Level

If you can afford the cost of a new database each time you add a new tenant to your application, this approach might be right for you. This approach is the slowest from all discussed architectures when it comes to adding new tenants.

It is most likely tough to lead to a data leak because to switch to another tenant, you have to establish a new connection to the database. You can also easily modify the database structure of every group of users if you would like to.

Creating an Application Skeleton

We will create two separate applications: one with multi-tenancy supported by one database and one with multi-tenancy supported by multiple databases. They represent a different approach to deal with numerous tenants, but the configuration phase is the same.

Ruby’s latest version is 2.7.2 and 6.1 RC1 of Ruby on Rails gem as of writing this article. Make sure you have the right version of Ruby installed in your system and attempt to install the correct version of the framework:

1
gem install rails -v 6.1.0.rc1

We can now generate the project files with the following command:

1
rails _6.1.0.rc1_ new tenantapp -d=mysql

Enter the project directory, run the server, and check if you can see the welcome screen so we can continue:

1
2
cd tenantapp/
rails s

Multi-tenant Rails Application with One Database

It’s time to code something. This part of the article will show you how to implement multitenancy in your Rails application. I’m going to use the approach with one database and one schema.

The plan is as follows:

Preparing the Test Data

Let’s assume that we are building a blog platform where users can view articles published by multiple authors.

I will create the author model first, which will store the author’s data:

1
2
3
rails g scaffold author slug:string name:string description:string
rake db:create
rake db:migrate

You can now visit http://localhost:3000/authors address and add a few authors so we can later assign them to the articles.

The next step is to create articles:

1
2
rails g scaffold article title:string content:text
rake db:migrate

Adding Multitenancy Support

In the application we are creating, authors are our tenants. Each author should have access only to his articles. As I mentioned before, when we want to implement multitenancy support in one database and one schema, the critical requirement is to add the tenant_id field to every model that is going to be managed by our tenants.

Create proper migrations in the database

1
2
rails g migration AddTenantIdToArticle tenant_id:integer
rake db:migrate

The above migration will add the new column to our Article model. This column will be used in any query to pull access only data assigned to the current tenant.

Assign article to the tenant

In this step, I’m going to update the code so we can assign the given author to the article and later render articles only for the selected author. Open app/controllers/articles_controller.rb and add the following changes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ArticlesController < ApplicationController
 before_action :set_article, only: [:show, :edit, :update, :destroy]
 before_action :set_authors, only: [:edit, :update, :new, :create]

 # ...

 private
   # Only allow a list of trusted parameters through.
   def article_params
     params.require(:article).permit(:title, :content, :tenant_id)
   end

   def set_authors
     @authors = Author.all
   end
end

In our view we can now use the @authors variable which contains the collection of authors added in our app. We can now add the select field that will contain authors’ names and assign a proper tenant_id. Open app/views/articles/_form.html.erb and add the following section:

1
2
3
4
<div class="field">
 <%= form.label :author %>
 <%= form.select :tenant_id, options_from_collection_for_select(@authors, 'id', 'name', article.tenant_id), include_blank: true %>
</div>

Go ahead and create a few authors and then some articles to later render articles assigned only to the given author.

Adding Support for Custom Subdomains

We created the author with the name John Doe and set the slug value to johndoe. Our goal is to visit http://johndoe.localhost:3000/ address and see the data related only to the given tenant, John Doe, in this case.

Subdomain configuration

We would like to manage and visit articles when the tenant is set. We can achieve this by updating the config/routes.rb file and wrapping the definition of the articles resource into the constraints block:

1
2
3
4
5
6
7
Rails.application.routes.draw do
 constraints subdomain: /.*/ do
   resources :articles
 end

 resources :authors
end

By default, Rails set the top-level domain length to 1, but we want to use localhost to set this setting to 0. We can do this by adding the following line to the file config/environments/development.rb:

1
config.action_dispatch.tld_length = 0

Making subdomains work with the multitenancy

Now, it’s possible to assign any author to the article. It shouldn’t be possible when the subdomain is used. We have to alter the behavior of ArticlesController, and instead of setting all authors, we have to set the author for which the subdomain was requested:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class ArticlesController < ApplicationController
 before_action :set_author
 before_action :set_article, only: [:show, :edit, :update, :destroy]

 # GET /articles
 # GET /articles.json
 def index
   @articles = Article.where(tenant_id: @author.id)
 end

 # ...

 private
   def set_article
     @article = Article.find_by!(id: params[:id], tenant_id: @author.id)
   end

   # ...

   def set_author
     @author = Author.find_by!(slug: request.subdomain)
   end
end

I made a few changes to the controller:

When you work on a multitenant app and use one database with one schema, you always have to remember to scope any query by tenant_id column; otherwise, you will provide data that is not assigned to the requested tenant.

The controller is updated, so the next step is to update the form view. Open app/views/articles/_form.html.erb file and replace the previous section with the simple hidden field:

1
<%= form.hidden_field :tenant_id, value: @author.id %>

Before those changes, the user was able to select the author of the article using the form. The user can choose the author for all actions by using a given subdomain in the website address.

You can now test the code we created. Create a new author, for example, with the name John Doe and slug johndoe. Visit http://johndoe.localhost:3000/articles/new address and add a new article. After adding a new article, you can view it on the list that is available under http://johndoe.localhost:3000/articles.

Congratulations! You have just created a multi-tenant app where each author has its subdomain.

Further Improvements

You can now extend the code of the application by adding new features. Our blog platform is straightforward; maybe it’s time to add some comments section? No matter what functionality you add, you must remember to add the tenant_id column to the new model and use it when querying the resource.

You can create a special scope and wrap it into the concern:

1
2
3
4
5
6
7
module Tenantable
 extend ActiveSupport::Concern

 included do
   scope :for_author, -> (author) { where(tenant_id: author.id) }
 end
end

and then use it in every model that contains data for tenants:

1
2
3
4
5
6
class Article < ApplicationRecord
 include Tenantable
end

author = Author.find(1)
Article.for_author(author)

With the above approach, you would have to update only one place in case of renaming the tenant_id column or introducing more conditions when it comes to querying tenant-related data.

Multi-tenant Rails Application with Multiple Databases

In the previous paragraphs, we built a Rails application that supports multiple tenants with one database and one schema. The most significant disadvantage of such an approach is the high possibility of a data leak.

The good news is that the newest version of the Ruby on Rails framework has built-in support for managing multiple databases. I will explore it and show you how you can use it to build a multi-tenant Rails application with numerous databases and custom subdomains.

The plan is as follows:

Preparing the Test Data

Let’s assume that we are building a blog platform where users can view articles published by multiple authors. Each author will have a dedicated database.

Start with creating the Author model along with scaffolded features that will help us to view and create new authors:

1
2
3
rails g scaffold author slug:string name:string description:string
rake db:create
rake db:migrate

You can now visit http://localhost:3000/authors and see what Rails have generated for us.

Adding a new author

I created a new author with the name John Doe and slug johndoe. The slug value will be used later to detect which author we should display the information using the subdomain.

Because each author will have a separate database, we have to add manually new database for John. Open config/database.yml and add the following changes:

1
2
3
4
5
6
7
8
development:
 primary:
   <<: *default
   database: tenantapp_development
 primary_johndoe:
   <<: *default
   database: tenantapp_johndoe_development
   migrations_paths: db/tenants_migrations

We are going to use a separate directory for migrations that are used by our tenants. We don’t need them in the central database. You can now create a database for John by using the default command:

1
rake db:create

Adding articles

We can now scaffold the Article model along with the controller and views:

1
rails g scaffold article title:string content:text --database primary_johndoe

I passed the --database param to let Rails know that the migration shouldn’t be placed in the default db/migrations directory used by the primary database. We can now run the migrate command:

1
rake db:migrate

Right now, we have two schemas: db/schema.rb and db/primary_johndoe_schema.rb. If you would like to create different tables for tenants, you can achieve this by setting a unique migrations_path value in the config/database.yml file for the given tenant. In this article, we want to have the same tables for all tenants, so the path to migrations will be the same.

Adding Multi-tenancy Support

In the application where multi-tenancy is supported by one database, to get data for the given tenant, we just have to update the query to the database with the proper tenant_id value. In our case, each tenant has their database, so we have to switch between databases.

Rails 6.1 comes with built-in support for horizontal sharding. Shard is a horizontal data partition that contains a subset of the total dataset. We could store articles for all authors in one table in one database, but thanks to the sharding, we can split the data into multiple tables with the same structure but placed in separate databases.

Let’s define our shards in the parent class for all models. They are stored under app/models/application_record.rb:

1
2
3
ActiveRecord::Base.connected_to(role: :reading, shard: :johndoe) do
  Article.all # get all articles created by John
end

Without wrapping our call into the connected_to block, the default shard will be used. Before we move forward, we have to introduce one more change. Since all shards share the same data structure, we can delete the app/models/primary_johndoe_record.rb model created automatically when we were scaffolding articles.

We also have to edit the app/models/article.rb model and change the parent class from PrimaryJohndoeRecord to ApplicationRecord:

1
2
class Article < ApplicationRecord
end

Adding more tenants

Currently, we have only one author in our database. To test the functionality of switching between our tenants (databases), we have to add one more author. Open http://localhost:3000/authors/new address and add a new author. I added the author with the name Tim Doe and slug timdoe.

We have a record for the new author, so we have to define a new database:

1
2
3
4
5
6
7
8
9
10
11
12
development:
 primary:
   <<: *default
   database: tenantapp_development
 primary_johndoe:
   <<: *default
   database: tenantapp_johndoe_development
   migrations_paths: db/tenants_migrations
 primary_timdoe:
   <<: *default
   database: tenantapp_timdoe_development
   migrations_paths: db/tenants_migrations

Now create a new database and run migrations:

1
2
rake db:create
rake db:migrate

The last step is to update ApplicationRecord model and define a new shard:

1
2
3
4
5
6
7
8
9
class ApplicationRecord < ActiveRecord::Base
 self.abstract_class = true

 connects_to shards: {
   default: { writing: :primary, reading: :primary },
   johndoe: { writing: :primary_johndoe, reading: :primary_johndoe },
   timdoe: { writing: :primary_timdoe, reading: :primary_timdoe },
 }
end

You can now create articles for each author:

1
2
3
4
5
6
7
ActiveRecord::Base.connected_to(role: :writing, shard: :johndoe) do
  Article.create!(title: 'Article from John', content: 'content')
end

ActiveRecord::Base.connected_to(role: :writing, shard: :timdoe) do
  Article.create!(title: 'Article from Tim', content: 'content')
end

Adding Support for a Custom Subdomain

We can’t visit the articles list for the given author yet because we don’t know when we should show articles from John and Tim. We will solve this problem by implementing custom subdomains. When visiting http://johndoe.localhost:3000/articles, we should see articles from John, and when visiting http://timedoe.localhost:3000/articles articles from Tim.

Subdomain configuration

We would like to manage and visit articles when the tenant is set. We can achieve this by updating the config/routes.rb file and wrapping the definition of the articles resource into the constraints block:

1
2
3
4
5
6
7
Rails.application.routes.draw do
 constraints subdomain: /.*/ do
   resources :articles
 end

 resources :authors
end

By default, Rails set the top-level domain length to 1, but we want to use localhost to set this setting to 0. We can do this by adding the following line to the file config/environments/development.rb:

1
config.action_dispatch.tld_length = 0

Making subdomains work with the multitenancy

To standardize reading the database for the current tenant, I will create a controller concern called Tenantable. It provides the read_with_tenant method, which accepts a block and executes it in the context of the requested tenant:

1
2
3
4
5
6
7
8
9
10
11
12
13
module Tenantable
 extend ActiveSupport::Concern

 private

 def read_with_tenant(&block)
   author = Author.find_by!(slug: request.subdomain)

   ActiveRecord::Base.connected_to(role: :reading, shard: author.slug.to_sym) do
     block.call
   end
 end
end

Save this file as app/controllers/concerns/tenantable.rb and include in ArticlesController:

1
2
3
4
5
6
7
8
9
10
11
12
class ArticlesController < ApplicationController
 include Tenantable

 before_action :set_article, only: [:show, :edit, :update, :destroy]

 def index
   read_with_tenant do
     @articles = Article.all
   end
 end
 # ...
end

Now you can visit http://johndoe.localhost:3000/articles or http://timdoe.localhost:3000/articles, and you will see different articles displayed on the list.

If you would like to create new articles using the form, you have to define a new method called write_with_tenant and update the Tenantable concern and methods inside the ArticlesController accordingly.

Further Improvements

The approach presented above is just a simple wrapper method that executes code wrapped into a block within a given connection to the database. To make it more universal, you can create a middleware that will parse the subdomain and establish a connection before executing any code:

1
ActiveRecord::Base.establish_connection(:primary_timdoe)

The final solution depends on your needs and the number of places where you want to use the assigned data to a specific tenant.

Summary

Congratulations, you just built two versions of a multi-tenant Rails application and gained knowledge about the different ways of dealing with multiple tenants in a modern web application.

Let’s just quickly summarize what we have learned during this article:

I hope you enjoyed reading this article and building multi-tenant Ruby on Rails applications.

P.S. If you’d like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!

Our guest author Paweł is an open-source fan and growth seeker with over a decade of experience in writing for both human beings and computers. He connects the dots to create high-quality software, valuable relations with people and businesses.

Latest Ruby Magic articles (see all)

10 latest articles

Go back
Ruby magic icon

Subscribe to

Ruby Magic

Magicians never share their secrets. But we do. Sign up for our Ruby Magic email series and receive deep insights about garbage collection, memory allocation, concurrency and much more.

We'd like to set cookies, read why.