Verecom Blog

Innovations that will never ends at all

Verecom is an innovative IT service & solution provider.

30 Nov

How to create a multi-tenant rails application (subdomain approach)

In this article we will cover the basics of creating a multi-tenant rails application. The application is gonna be a blog as a service application. We will be using Devise for authentication and CanCanCan for authorization. Each user will be able to create multiple blogs which belongs to him/her. Each blog will have its own subdomain.

First of all, let's create a new rails app. I'm gonna assume that you already have a rails environment setup.

$ rails new MyMultiTenantBlog

$ cd MyMultiTenantBlog

Let's say the domain is mmtb.com

Next, we are going to create the User model with Devise
Add the following to your Gemfile:

gem 'devise'

And run the following commands to install devise and generate our model

$ bundle install
$ bundle exec rails generate devise:install
$ bundle exec rails g devise User
$ bundle exec rake db:migrate


Ok time to add our Blog model

$ bundle exec rails g scaffold Blog slug:string:index name:string

Also add some validations to make sure the subdomains and blog names are unique.

/models/blog.rb

validates :subdomain, uniqueness: { case_sensitive: false }
validates :name, uniqueness: { case_sensitive: false }

Create the many-to-many relationship between blogs and users. In your application, you might want to do that with a model so that you can easily add extra fields on the relationship later. For example, each user will have different role in different blog.

$ bundle exec rails g migration blogs_users

the migration file

class BlogsUsers < ActiveRecord::Migration
  def change
    create_table 'blogs_users', id: false do |t|
      t.column :blog_id, :integer
      t.column :user_id, :integer
    end
  end
end

run the migration

`$ bundle exec rake db:migrate`

models/user.rb

has_and_belongs_to_many :blogs

models/blog.rb

has_and_belongs_to_many :users


Let's work on authorization now.

Add the following to your Gemfile:

gem 'cancancan'

Install the gem and generate the ability file.

`$ bundle install`
`$ bundle exec rails g cancan:ability`

Anonymouse visitors will be able to read the blogs only, while users will be able to create new blog and update/delete the blogs that they created.


/models/ability.rb

class Ability
  include CanCan::Ability

  def initialize(user)
    if user
      can [:read, :create], Blog
      can [:update, :destroy], Blog do |blog|
        blog.users.include? user
      end
    else
      can :read, Blog
    end
  end
end


/controllers/blogs_controller

  load_and_authorize_resource


Here's our big-time. There are several gems available to easily implement multi-tenancy. You may find a list of them from The Ruby Toolbox. We are going to use the apartment gem for this project.

Add the following to your Gemfile:

gem 'apartment'

Then install it and generate our Apartment config file with the following commands:

`$ bundle install`
`$ bundle exec rails generate apartment:install`

User and Blog will be shared across the application, so let's exclude the models. Meanwhile, Blog.subdomain will used to determine the tenant

/config/apartment.rb

  config.excluded_models = %w{ User Blog }
  config.tenant_names = lambda { Blog.pluck :subdomain }

We also want to make sure the tenant will be created right after a blog get created

/models/blog.rb

  after_create :create_tenant

  private

  def create_tenant
    Apartment::Tenant.create(slug)
  end

We also need two set of routes. One for the tenants. For example myblog.mymultitenantblog.com. The other for the main site www.mymultitenantblog.com where will have our landing pages, home page, sign up page and so on.

/config/routes

class slugConstraint
  def self.matches?(request)
    slugs = %w( www admin )
    request.slug.present? && !slugs.include?(request.slug)
  end
end

class MaindomainConstraint
  def self.matches?(request)
    slugs = %w( www admin )
    !request.slug.present? || slugs.include?(request.slug)
  end
end


  constraints slugConstraint do
  end
  constraints MaindomainConstraint do
    resources :blogs
    devise_for :users
  end

You should probably refactor the routes a little bit.

All right. We probably don't want to have our users to login every time they switch to a new subdomain. So let's make the session cookie to be stored on the top level of our domain.

/config/session_store.rb

Rails.application.config.session_store :cookie_store, key: '_MyMultiTenantBlog_session', domain: 'mymultitenantblog.com'

Ok. Now we should add the Post model

$ bundle exec rails g scaffold Post title:string
$ bundle exec rake db:migrate

And we should update the ability.rb for post

/models/ability.rb

current_blog = Blog.find_by(slug: Apartment::Tenant.current)

    if user
      can :read, Post
      if current_blog.users.include? user
        can [:create, :update, :destroy], Post
      end

    else
      can :read, Post

    end

Also remember to update the routes

You may find the final code here: https://github.com/Verecom/MyMultiTenantBlog