Skip to content

Multitenancy

Multitenancy is a very talked-about subject. We're not going to go very deep into how to achieve it on the database level, but will talk a little bit about how it's supported in Avo.

Breakdown

Usually, with multitenancy you add a new layer just one level below authentication. You don't have just a user to think about, but now that user might act on the behalf of a tenant. That tenant can be an Account or a Team, or any other model you design in your database.

So now, the mission is to pinpoint which tenant is the user acting for. Because Avo has such an integrated experience and we use our own ApplicationController, you might think it's difficult to add that layer, when in fact it's really not. There are a couple of steps to do.

INFO

We'll use the foo tenant id from now on.

Route-based tenancy

There are a couple of strategies here, but the a common one is to use route-based tenancy. That means that your user uses a URL like https://example.com/foo/ and the app should know to scope everything to that foo tenant.

We need to do a few things:

1. Disable automatic Avo engine mounting

Do this step only if you use other Avo gems (avo-pro, avo-advanced, etc.)

Avo will automatically mount it's engines unless you tell it otherwise, which is what we'll do now.

ruby
Avo.configure do |config|
  # Disable automatic engine mounting
  config.mount_avo_engines = false

  # other configuration
end

Related:

2. Set the proper routing pattern

ruby
# Mount Avo and it's engines under the `tenant_id` scope
scope "/:tenant_id" do
  mount Avo::Engine, at: Avo.configuration.root_path

  scope Avo.configuration.root_path do
    instance_exec(&Avo.mount_engines)
  end

3. Set the tenant for each request

ruby
Avo.configure do |config|
  # configuration values
end

Rails.configuration.to_prepare do
  Avo::ApplicationController.include Multitenancy
end
ruby
module Multitenancy
  extend ActiveSupport::Concern

  included do
    prepend_before_action :set_tenant
  end

  def set_tenant
    Avo::Current.tenant_id = params[:tenant_id]
    Avo::Current.tenant = Account.find params[:tenant_id]
  end
end

Now, whenever you navigate to https://example.com/lol the tenant the tenant_id will be set to lol.

Session-based tenancy

Using a session-based tenancy strategy is a bit simpler as we don't meddle with the routing.

WARNING

The code below shows how it's possible to do session-based multitenancy but your use-case or model names may vary a bit.

We need to do a few things:

1. Set the tenant for each request

ruby
Avo.configure do |config|
  # configuration values
end

Rails.configuration.to_prepare do
  Avo::ApplicationController.include Multitenancy
end
ruby
module Multitenancy
  extend ActiveSupport::Concern

  included do
    prepend_before_action :set_tenant
  end

  def set_tenant
    Avo::Current.tenant = Account.find session[:tenant_id] || current_user.accounts.first
  end
end

2. Add an account switcher

Somewhere in a view on a navbar or sidebar add an account switcher.

erb
<% current_user.accounts.each do |account| %>
  <%= link_to account.name, switch_account_path(account.id), class: class_names({"underline": session[:tenant_id].to_s == account.id.to_s}), data: {turbo_method: :put} %>
<% end %>
ruby
class Avo::SwitchAccountsController < Avo::ApplicationController
  def update
    # set the new tenant in session
    session[:tenant_id] = params[:id]

    redirect_back fallback_location: root_path
  end
end