Skip to content

Acts As Tenant Integration

Recipe contributed by SahSantoshh.

WARNING

The guide expressed here shows how you we can add subdomain-level multitenancy (sah.example.org, adrian.example.org, etc).

This makes for more than one URL per application which in turn requires a special license. To get more information please reach out to us.

There are different ways to achieve multi-tenancy in an application. We already have a doc which describes about Multitenancy with Avo. Here we will deep dive in integrating Acts As Tenant which supports row-level multitenancy with Avo. In this implementation we will be setting tenant to subdomain.

INFO

Check out the acts_as_tenant documentation for reference.

Installation

To use it, add it to your Gemfile:

ruby
gem 'acts_as_tenant'

Tenant

Let's create model for tenant. We are using Account as our tenant.

Account Migration and Model class

ruby
# Migration
class CreateAccounts < ActiveRecord::Migration[7.1]
  def change
    create_table :accounts do |t|
      t.string :name
      t.string :subdomain

      t.timestamps
    end

    add_index :accounts, :subdomain, unique: true
    add_index :accounts, :created_at
  end
end
ruby
# Account model handles Tenant management
class Account < ApplicationRecord
  MAX_SUBDOMAIN_LENGTH = 20

  validates :name, :subdomain, presence: true
  validates_uniqueness_of :name, :subdomain, case_sensitive: false
  validates_length_of :subdomain, :name, maximum: MAX_SUBDOMAIN_LENGTH

end

Scope models


Now let's add users to Account. Here I am assuming to have an existing user model which is used for Authentication. Similarly we can scope other models.

ruby
class AddAccountToUsers < ActiveRecord::Migration
  def up
    add_column :users, :account_id, :integer # if we have existing user set null to true then update the data using seed
    add_index  :users, :account_id
  end
end
ruby
# Authentication
class User < ActiveRecord::Base
  acts_as_tenant(:account)
end

Setting the current tenant

There are three ways to set the current tenant but we be using the subdomain to lookup the current tenant. Since Avo has it's own Application Controller so there is no point in setting the tenant in Rails default Application Controller but we will set it there as well just to be safe site and also we might have some other pages other than Admin Dashboard supported by Avo.

ruby
# Multitenancy, to set the current account/tenant.
module Multitenancy
  extend ActiveSupport::Concern

  included do
    prepend_before_action :set_current_account
  end

  def set_current_account
    hosts = request.host.split('.')

    # just to make sure we are using subdomain path
    subdomain = (hosts[0] if hosts.length > 2)

    # We only allow users to login from their account specific subdomain not outside of it.
    sign_out(current_user) if subdomain.blank?

    current_account = Account.find_by(subdomain:)
    sign_out(current_user) if current_account.blank?

    # set tenant for Avo and ActAsTenant
    ActsAsTenant.current_tenant = current_account
    Avo::Current.tenant = current_account
    Avo::Current.tenant_id = current_account.id
  end
end
ruby
Avo.configure do |config|
  # configuration values
end

Rails.configuration.to_prepare do
  Avo::ApplicationController.include Multitenancy
end

Now, whenever we navigate to https://sahsantoshh.example.com/ the tenant & the tenant_id will be set to sahsantoshh.

Move existing data to model

We might have to many users and other records which needs to be associated with Account. For example, we will only move users record to the account

ruby
# Create default/first account where we want to associate exiting data
account = Account.find_or_create_by!(name: 'Nepal', subdomain: 'sahsantoshh')

User.unscoped.in_batches do |relation|
  relation.update_all(account_id: account.id)
  sleep(0.01) # throttle
end

Now run the seed command to update existing records