ruby

Building a Multi-tenant Ruby on Rails App With Subdomains

Paweł Dąbrowski

Paweł Dąbrowski on

Last updated:

Building a Multi-tenant Ruby on Rails App With Subdomains

This post was updated on 4 August 2023 with code updates and to reference the latest version of Rails, Rails 7.

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:

  • Discuss different types of multi-tenant apps and tell when the given approach is the right choice,
  • Create a Ruby on Rails application with support for multiple tenants, and
  • Use custom subdomains for your customers inside the Ruby on Rails application.

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:

  • The first one will be a Rails app with multi-tenancy supported by one database.
  • The second app will be a Rails app with multi-tenancy supported by multiple databases.

Each app represents a different approach to dealing with numerous tenants, but the configuration phase is the same. We'll be using the latest version of Ruby on Rails as of this writing which is Rails version 7.0.6.

Also make sure you have the right version of Ruby installed in your system. For this tutorial, we'll be using Ruby version 3.2.2:

shell
gem install rails -v 7.0.6

With that, we are now ready to get started on the first app.

Multi-tenant Rails Application with One Database

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:

  • We will create a skeleton for a Rails application using the newest available version of Ruby and Ruby on Rails framework, Rails 7.
  • We will scaffold some models to have the data we can operate on
  • To the functionality we created in the previous step, we will add support for multitenancy by updating the database tables and code in our application
  • The last step would be to add support for custom subdomains to allow users to access their tenants easily
  • We will explore ideas for some features that you can implement later on your own

Generating a New Rails App

In the terminal, generate a new Rails app with the command below:

shell
rails new multi_tenant_one_db --javascript=esbuild --css=bulma

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:

shell
rails g scaffold author slug:string name:string description:text rails db:create && rails 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:

shell
rails g scaffold article title:string content:text rails 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

shell
rails g migration AddTenantIdToArticle tenant_id:integer rails 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:

ruby
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:

ruby
<div class="field"> <label class="label">Author</label> <div class="control"> <div class="select"> <%= form.select :tenant_id, options_from_collection_for_select(@authors, 'id', 'name', article.tenant_id), include_blank: true %> </div> </div> </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

Let's assume we've created an 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:

ruby
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:

ruby
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:

ruby
class ArticlesController < ApplicationController before_action :set_author before_action :set_article, only: [:show, :edit, :update, :destroy] 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:

  • Instead of the set_authors method, I defined the set_author method, which set the author requested via the subdomain. It is crucial to call this method in before_filter before the set_article is called. The author has to be assigned before we attempt to set the article.
  • I updated the set_article method to look for the article with the given id and the assigned author. We don’t want to render articles created by Tom while our current tenant is John.
  • I updated the index action to select only articles assigned to our current tenant.

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:

ruby
<%= 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 gets their own 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 a concern:

ruby
# app/controllers/concerns 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:

ruby
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.

And with that, you have a working multi-tenant Rails app using a single database.

Multi-tenant Rails Application with Multiple Databases

In the previous section, 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 since all tenant data is within a shared database/schema.

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:

  • We will create a skeleton for a Rails application using the newest available version of Ruby and Ruby on Rails framework
  • We will scaffold some models to have the data we can operate on
  • We will add support for custom subdomains.
  • We will learn how to add new tenants by creating and configuring new databases.
  • The last step would be to update the application to switch the database when the given subdomain is requested.

Generating a New Rails App

In the terminal, generate a new Rails app with the command below:

shell
rails new multi_db_multi_tenant_app --javascript=esbuild --css=bulma

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:

shell
rails g scaffold author slug:string name:string description:text

We could run the migration to create the database right away but since we want separate databases for each author, we'll hold on for now and first configure the multiple database connections.

Adding a new author

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

yaml
development: primary: <<: *default database: db/dev_multitenant_app.sqlite3 primary_johndoe: <<: *default database: db/johndoe_dev_multitenant_app.sqlite3 migrations_paths: db/tenants_migrations

Then run the following commands:

shell
rails db:create rails db:migrate

After that, go ahead and add 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. Next, let's deal with the articles.

Adding articles

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

shell
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:

shell
rails 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 7 has 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. Open up app/models/application_record.rb and edit it to look like the code below:

ruby
# app/models/application_record.rb class ApplicationRecord < ActiveRecord::Base self.abstract_class = true ActiveRecord::Base.connected_to(role: :reading, shard: :johndoe) do Article.all # get all articles created by John end 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:

ruby
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:

yaml
development: primary: <<: *default database: db/dev_multitenant_app.sqlite3 primary_johndoe: <<: *default database: db/johndoe_dev_multitenant_app.sqlite3 migrations_paths: db/tenants_migrations primary_timdoe: <<: *default database: db/timdoe_dev_multitenant_app.sqlite3 migrations_paths: db/tenants_migrations

Now create a new database and run migrations:

shell
rails db:create rails db:migrate

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

ruby
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:

ruby
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 articles from Tim when visiting http://timdoe.localhost:3000/articles.

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:

ruby
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:

ruby
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:

ruby
# app/controllers/concerns/tenantable.rb 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:

ruby
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:

ruby
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:

  • There are three primary multi-tenancy levels in a web application - database-row, schema, and database level.
  • Each approach has its advantages and disadvantages, and your choice depends on the hardware possibilities and the level of isolation of the tenant information.
  • It is possible to implement all multi-tenancy levels within the Ruby on Rails framework without any external libraries.
  • Ruby on Rails supports custom subdomain out of the box, so it’s a perfect addition for a multi-tenant application where each tenant can have its subdomain assigned.

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!

Paweł Dąbrowski

Paweł Dąbrowski

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 and build valuable relations with people and businesses.

All articles by Paweł Dąbrowski

Become our next author!

Find out more

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps