Implementing Multi-Tenancy in an `existing` Ruby On Rails (6+, 7+)Applications Using Apartment Gem and Subdomains (Part 1/3)

Hamzawais
4 min readMar 22, 2024

Introduction

In the realm of SaaS (Software as a Service) applications, multi-tenancy is a pivotal architecture that allows a single instance of the software to serve multiple tenants. Each tenant’s data is isolated and remains invisible to other tenants. In this article, I’ll share my journey of integrating multi-tenancy into an existing Rails application using the Apartment gem, covering the challenges faced and solutions found along the way.

Table of Content:

  1. What is multi-tenancy?
  2. What is a tenant?
  3. What are different types of multi-tenants applications? (Single database, multi-database, multi-schema)
  4. Downsides of multi-schemas?
  5. Assumptions and requirements
  6. Setting up basics of subdomains
  7. Integrating Apartment Gem and use Multi-schema methodology.
  8. Migrating existing data (optional)

This is going to be a comprehensive guide to integrate Multi-tenancy in your (new or existing) Rails based application, while using subdomains to differentiate between the tenants. It is going to be a multi-part article, so hang tight.

What is multi-tenancy?

Multi-tenancy is an architecture where a single instance of software serves multiple users or groups (tenants), with each tenant’s data isolated and invisible to other tenants. This approach is common in cloud computing and SaaS (Software as a Service) applications, allowing for efficient resource utilization and simplified maintenance.

What is a tenant?

A tenant in the context of multi-tenancy refers to a user or group of users who share a common access with specific privileges to the software instance. Each tenant’s data and configuration can be customized and kept separate from other tenants, ensuring privacy and security.

What are different types of multi-tenant applications?

  • Single Database: All tenants share a single database, with data differentiation typically managed through tenant IDs in the tables.
  • Multi-Database: Each tenant has its own database, ensuring complete data isolation but increasing complexity in maintenance and scaling.
  • Multi-Schema: Similar to the single database approach but uses database schemas to separate tenant data within the same database, offering a balance between isolation and manageability.

Downsides of multi-schemas?

  • Complexity in Schema Management: Managing migrations and schema changes across multiple schemas can be complex and time-consuming.
  • Resource Utilization: While more efficient than multi-database approaches, multi-schema setups can still lead to increased resource consumption as the number of tenants grows.
  • Cross-Tenant Operations Difficulty: Performing operations across multiple tenants or aggregating data from multiple schemas can be challenging and may require additional tools or custom development.

Assumptions and requirements

I will be assuming that you already have a rails project set up, with sidekiq, postgres, and action cable hooked up.

Setting up basics of subdomains

Let’s assume that name of our tenant is Clinic. First of all, we will add subdomain column to our table.

class AddSubDomainToClinic < ActiveRecord::Migration[6.0]
def up
add_column :clinics, :subdomain, :string
Clinic.all.each do |clinic|
clinic.update(subdomain: "clinic-#{clinic.id}")
end
end

def down
remove_column :clinics, :subdomain
end
end

SubdomainConstraint

# app/constraints/sub_domain_constraint.rb

class SubdomainConstraint
def self.matches?(request)
request.subdomain.present? && Clinic.exists?(subdomain: request.subdomain)
end
end
# config/application.rb  
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.autoload_paths << Rails.root.join('constraints')
# rest of your code
end

The SubdomainConstraint class in the app/constraints/sub_domain_constraint_test.rb file is a custom routing constraint used in a Ruby on Rails application to determine if a request should be routed to a specific part of the application based on the presence and validity of a subdomain.

Here’s a brief explanation of its components:

  • Purpose: The class is designed to check if an incoming request’s subdomain is present and corresponds to a valid Clinic record in the database. This is useful in multi-tenant applications where each tenant (in this case, each clinic) is accessed via a unique subdomain.
  • matches? Method: This class method is the core of the constraint. It takes a request object as its argument and performs two checks on it
  • request.subdomain.present? verifies that the request includes a subdomain. This is important because the root domain (without a subdomain) might be used for general application access or administrative purposes, and not tied to a specific tenant.
  • Clinic.exists?(subdomain: request.subdomain) checks if there is a Clinic record in the database with a subdomain that matches the request's subdomain. This ensures that the subdomain is not only present but also corresponds to an existing tenant.
  • Usage: This constraint is typically used in the application’s routing configuration (config/routes.rb) to conditionally route requests to different parts of the application based on the subdomain. For example, it can be used to direct requests to tenant-specific controllers and actions, ensuring that users are served content relevant to their specific clinic.
# routes.rb

Rails.application.routes.draw do
mount ActionCable.server => '/cable'

constraints SubDomainConstraint do
# your routes here
end

devise_for :staff_users, :skip => [:registrations]

resources :impersonates, only: [:create, :destroy]
end

Subdomain Constraints: The constraints SubDomainConstraint do ... end block is used to scope certain routes to only be accessible if they meet the criteria defined in the SubDomainConstraint class. This is particularly useful in multi-tenant applications where you might want to restrict access or functionality based on the subdomain of the request. Inside this block, you would define routes that are specific to tenants, ensuring that these routes are only accessible when the request's subdomain matches an existing tenant in your application.

Now you should have your subdomain running on: http://valid-subdomain.lvh.me:3000/

In the next part, we will discuss how we can install Apartment, and update our codebase to support multi-tenancy on Database level.

Part 2: https://hamzawais54.medium.com/implementing-multi-tenancy-in-an-existing-ruby-on-rails-6-7-applications-using-apartment-gem-9ea23935a3b8

You get in touch with me:

email: hamzawais54@gmail.com | Phone: +923244105651 | web: meet-hamza.com | linkedin: https://www.linkedin.com/in/hamza-awais-1908/

--

--