Mastering Multi-Tenancy in Rails: Boost Your SaaS with PostgreSQL Schemas

Multi-tenancy in Rails using PostgreSQL schemas separates customer data efficiently. It offers data isolation, resource sharing, and scalability for SaaS apps. Implement with Apartment gem, middleware, and tenant-specific models.

Mastering Multi-Tenancy in Rails: Boost Your SaaS with PostgreSQL Schemas

Implementing multi-tenancy in Ruby on Rails for SaaS applications using PostgreSQL schemas is a game-changer. It’s like giving each of your customers their own private playground within your app. Let’s dive into this exciting world and see how we can make it happen.

First things first, what exactly is multi-tenancy? Imagine you’re running an apartment building. Each tenant has their own space, but they’re all part of the same structure. That’s multi-tenancy in a nutshell. In the software world, it means multiple customers (tenants) use the same application, but their data is kept separate and secure.

Now, why use PostgreSQL schemas for this? Well, schemas are like containers within your database. They group tables and other database objects together, keeping things neat and tidy. It’s perfect for separating tenant data without the overhead of creating entirely new databases for each customer.

Let’s roll up our sleeves and get coding. We’ll start by setting up our Rails application to work with PostgreSQL schemas. First, we need to add the ‘apartment’ gem to our Gemfile:

gem 'apartment'

Don’t forget to run bundle install after adding the gem.

Next, we need to configure the apartment gem. Create a new file config/initializers/apartment.rb and add the following:

Apartment.configure do |config|
  config.excluded_models = ['User', 'Role']
  config.tenant_names = lambda { Company.pluck(:subdomain) }
end

This configuration tells Apartment which models should be shared across all tenants (like User and Role) and how to determine the tenant names (in this case, we’re using Company subdomains).

Now, let’s create a middleware to set the correct tenant for each request. Create a new file app/middleware/tenant_middleware.rb:

class TenantMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    request = Rack::Request.new(env)
    tenant = request.subdomain
    Apartment::Tenant.switch!(tenant) if tenant.present?
    @app.call(env)
  end
end

Add this middleware to your config/application.rb:

config.middleware.use TenantMiddleware

Great! Now our app is set up to handle multi-tenancy. But how do we actually create and manage tenants? Let’s create a Company model to represent our tenants:

rails generate model Company name:string subdomain:string

And let’s add some validations to our Company model:

class Company < ApplicationRecord
  validates :name, presence: true
  validates :subdomain, presence: true, uniqueness: true
end

Now, whenever we create a new Company, we need to create a new tenant. Let’s add an after_create callback to our Company model:

class Company < ApplicationRecord
  after_create :create_tenant

  private

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

This will automatically create a new PostgreSQL schema for each new Company.

But what about actually using this in our controllers? Here’s where it gets fun. Let’s say we have a Projects controller. We can now scope all our queries to the current tenant automatically:

class ProjectsController < ApplicationController
  def index
    @projects = Project.all
  end

  def create
    @project = Project.new(project_params)
    if @project.save
      redirect_to @project, notice: 'Project created successfully!'
    else
      render :new
    end
  end

  # Other actions...
end

Notice how we don’t need to do anything special here. The Project.all query will automatically only return projects for the current tenant. Magic!

But what if we want to do something across all tenants? Apartment gives us a neat way to do this:

Apartment::Tenant.each do |tenant|
  Apartment::Tenant.switch!(tenant) do
    # Do something for each tenant
    puts "Number of projects for #{tenant}: #{Project.count}"
  end
end

This is super useful for things like data migrations or generating reports across all your customers.

Now, let’s talk about some gotchas and best practices. First, always be mindful of your shared tables. If you’re not careful, you might accidentally expose data across tenants. That’s why we excluded User and Role in our Apartment configuration earlier.

Second, be careful with your associations. If you have a shared model (like User) that has_many of a tenanted model (like Projects), you need to be extra careful. You might want to consider using a join model that’s tenant-specific.

Third, think about how you’ll handle things like background jobs. You’ll need to make sure your jobs know which tenant they’re operating on. One way to do this is to pass the tenant as a parameter to your job:

class ProjectCleanupJob < ApplicationJob
  def perform(tenant)
    Apartment::Tenant.switch!(tenant) do
      Project.old.destroy_all
    end
  end
end

ProjectCleanupJob.perform_later(current_tenant)

Let’s talk about testing. When you’re testing a multi-tenant app, you need to make sure you’re in the right context. RSpec makes this easy with its use_transactional_fixtures option:

RSpec.configure do |config|
  config.use_transactional_fixtures = false

  config.before(:suite) do
    DatabaseCleaner.clean_with(:truncation)
  end

  config.before(:each) do |example|
    Apartment::Tenant.switch!('test')
    DatabaseCleaner.cleaning do
      example.run
    end
  end
end

This ensures that each test runs in its own clean tenant.

Now, let’s discuss some advanced topics. What if you want to allow users to belong to multiple tenants? One way to handle this is to have a shared User model, but use a join table to associate users with companies:

class User < ApplicationRecord
  has_many :memberships
  has_many :companies, through: :memberships
end

class Company < ApplicationRecord
  has_many :memberships
  has_many :users, through: :memberships
end

class Membership < ApplicationRecord
  belongs_to :user
  belongs_to :company
end

With this setup, you can easily check if a user has access to a particular tenant:

def set_tenant
  subdomain = request.subdomain
  company = Company.find_by(subdomain: subdomain)
  if current_user.companies.include?(company)
    Apartment::Tenant.switch!(subdomain)
  else
    redirect_to root_path, alert: 'You do not have access to this tenant'
  end
end

Another advanced topic is handling file uploads in a multi-tenant environment. If you’re using ActiveStorage, you might want to prefix your keys with the tenant name to keep files separated:

class Project < ApplicationRecord
  has_one_attached :avatar

  def avatar_key
    "#{Apartment::Tenant.current}/#{super}"
  end
end

This ensures that even if two tenants upload a file with the same name, they won’t overwrite each other.

Lastly, let’s talk about performance. As your app grows, you might start to see some slowdowns, especially if you have a lot of tenants. One way to mitigate this is to use connection pooling. The ‘makara’ gem works well with Apartment:

gem 'makara'

# In database.yml
production:
  adapter: 'makara_postgresql'
  makara:
    sticky: true
    connections:
      - role: master
        name: master
        database: my_database
      - role: slave
        name: slave1
        database: my_database
      - role: slave
        name: slave2
        database: my_database

This setup allows you to distribute your read queries across multiple database servers, potentially giving you a significant performance boost.

In conclusion, implementing multi-tenancy in Rails using PostgreSQL schemas is a powerful way to build scalable SaaS applications. It provides a good balance of data isolation and resource sharing, allowing you to efficiently serve multiple customers from a single application instance. While it does add some complexity to your app, the benefits in terms of scalability and data security are well worth it.

Remember, every app is unique, and what works for one might not work for another. Always consider your specific requirements and constraints when implementing multi-tenancy. And most importantly, have fun! Building multi-tenant apps can be challenging, but it’s also incredibly rewarding. Happy coding!