loading...

Multi-tenant apps on Ruby On Rails

matiascarpintini profile image Matias Carpintini Updated on ・4 min read

Please, stop using Apartment!

Before that, what is a multi-tenant app?

According to Wikipedia:

The term "software multitenancy" refers to a software architecture in which a single instance of software runs on a server and serves multiple tenants.

Multitenancy usually means handling many independent tenants from one app. A tenant is a group of users who share a common access with specific privileges to the software instance.

Most popular in SAAS models and, there are several types of implementation.

I found this article very helpful about the different approaches to multitenancy in Rails apps, please read it.


In my case, i have a CMS builder, where user can create their own blog, customize it, etc. This user can invite people to work on, the blog has an authentication module for their readers and so on.

The reader access to the blog thought a subdomain. Like, something.blogapp.com

I think with this example it is quite clear.

Whats wrong with Apartment Gem?

As i said in the beginning of this post, Apartment Gem is not an option anymore. I repeat it because, when i looking for information before solve the problem, i found a lot of current blog posts where people still are using/recommending it.

Apartment Gem are based on Schema-level approach. If you read the article i mention before, you now know about it, if no:


Every tenant has a separate namespace. In practice, we are creating more tables like:

  • tenant1.users
  • tenant2.users
  • tenant3.users

Let's see the pros & cons if this approach:

Pros:

  • No data leakages between tenants
  • Easy extraction of single-tenant data
  • No additional costs

Cons:

  • Less known PG feature & stateful search_path
  • A growing number of database tables. Heroku is against this approach when you are using their services (Heroku about Multiple Schemas)
  • Migrations run in O(n) complexity
  • Trouble on managed databases
  • Slower (create a schema, create tables)

Coming back with Apartment:

  • No support for Rails 6.0
  • Gem no longer supported
  • Code is not written optimally, found couple performance errors
  • Creators are not even encouraging it (Our multitenancy journey with Postgress schemas and Apartment)
  • Sometimes migrations are not run in some namespaces which leads to an inconsistent data structure

Summary: An earlier solution for multitenancy in Rails is still widely used even though it is not optimal in contemporary times.

  • Credits: you will find a guide to migrate from Apartment to ActiveRecord Multi Tenant (another gem) too

Finally, how i solve it

I choose the Row-level approach. With this, you have a tenant_id column in every DB table and filter by tenant_id in each query.

Pros: It's just another row in the table
Cons: It's just another row in the table 😂, i mean, you need to have tenant_id keys everywhere and if you forget the WHERE condition, you have leaking data.


I'm using Devise so, the tenant_id is my user_id (the same with Readers, i will skip that to avoid make it more longer). I also add a current_blog to the users table, to allow user have multiple blogs, and change between ones. This is how the User model looks like:

class User < ApplicationRecord
  belongs_to :current_blog, class_name: 'Blog', optional: true
end

Blog model,
The migration: $ rails g model Blog name:string:uniq owner:references

class Blog < ApplicationRecord
  belongs_to :owner, class_name: "User"
  has_many :readers, dependent: :destroy
end

Keep in mind: this is the final result, if you try to make a relationship with :readers before the table exits, you will get an error. Run all the migrations first.

Reader model
The migration: $ rails g model Reader username:string:uniq blog:references

class Reader < ApplicationRecord
  belongs_to :blog
  validates_uniqueness_of :username, scope: :blog_id
end

When i want to get the readers of the blog, i just add the tenant references to get the resource:

@readers = Blog.where(user: current_user.current_blog).readers

Subdomains

I solve it with constraints.

This is how my /config/routes.rb looks like:

require "subdomain.rb"

Rails.application.routes.draw do
  constraints(Subdomain) do
    scope module: :readers do
      root "readers#show", as: :subdomain_root
    end
  end

  # * everything else...
  end
end

The /lib/subdomain.rb:

class Subdomain
  def self.matches?(request)
    case request.subdomain
    when 'www', '', nil
      false
    else
      true
    end
  end
end

Then just i find the blog with a callback on my application_controller.rb:

class ApplicationController < ActionController::Base
  before_action :set_blog_if_subdomain
  def set_blog_if_subdomain
    if request.subdomain.present?
      @blog = Blog.find_by_name(request.subdomain)
      render_404 if !@blog
    end
  end

  def render_404
    render file: "#{Rails.root}/public/404.html", status: 404
  end
end

To test it on development, run the rails server with: $ rails s -p 3000 -b lvh.me, then you can access to http://blog-name.lvh.me:3000

In production you need a Wildcard. You can generate one for free with Let's Encrypt.

Just upload the cert (including the root domain) to Heroku and point everything on your DNS.

A CNAME record to: *, herokudns and an CNAME record to the root domain: @, herokudns.

Bye 👋

Discussion

pic
Editor guide
Collapse
jorgeddw profile image
Jorge Dominguez

Have you tried github.com/ErwinM/acts_as_tenant ?
What are your thought?

Collapse
matiascarpintini profile image
Matias Carpintini Author

I didn't try it, but it definitely seems like a good option. It is similar to the rudimentary way in which I solved it in my project, but it raises the scopes, and stores everything in concerns, leaving a much fancier code.

Collapse
brahimdahmani profile image
Brahim Dahmani

You must give credits to the original writers : realptsdengineer.com/ruby-on-rails...

Collapse
pedromschmitt profile image
Pedro Schmitt

Just to who still need the Apartment gem - there is a fork that is actively maintained and working with Rails 6: github.com/rails-on-services/apart...

Collapse
matiascarpintini profile image