6 min read

What Is Multitenancy? Rails Examples for SaaS

Hero image for What Is Multitenancy? Rails Examples for SaaS
Table of Contents

💡

Multitenancy isn’t “having users.” It’s how a SaaS isolates and routes each customer’s data so tenants can’t see or affect one another

Multitenancy basics

What you’ll learn: The difference between multitenancy and user management, and how data isolation and routing work

Multitenancy means one app instance serves many customers, called tenants (each tenant is a customer account), while keeping their data isolated. That’s the point: safe sharing of infrastructure without data leakage

Clients often say “multitenant” when they mean users and roles. Helpful, but not the same thing

  • Multitenancy: isolation and routing of tenant data across the whole stack
  • User management: logins, roles, and permissions inside one tenant’s data space
  • Proposal tip: ask, “How should each customer’s data be isolated?”

In short, user management sits inside a tenant; multitenancy decides where tenant data lives and how requests are routed

Transition: Now that terms are clear, let’s name the building blocks

Core building blocks

What you’ll learn: The key models and system concerns that make multitenancy work

Tenant vs user

  • Tenant model: School, Account, or Organization the customer container
  • User model: People who belong to a tenant with roles like admin, teacher, student

Brief term check:

  • Tenant: the customer account and its data boundary
  • Role-based access control (RBAC): permissions assigned to roles, then to users

Isolation and routing

  • Isolation: row-level filters in one database or separate databases per tenant
  • Routing: how the app detects the tenant subdomain, custom domain, header, or token

Quick definitions:

  • Row-level multitenancy: filtering records by tenant_id so queries only see the current tenant’s rows
  • Database-per-tenant: each tenant gets its own database; the app switches connections

Infra and security

  • Infrastructure: background jobs, caching, and logs must include tenant context
  • Security: prevent cross-tenant reads and writes, including in jobs and edge cases

Think: which apartment is this request for, and where are its walls

Transition: With the parts named, choose a pattern

Two patterns

What you’ll learn: When to use row-level vs database-per-tenant and the trade-offs

There are two mainstream patterns. Choose by isolation needs, scale, and operations budget

DimensionRow-level (single DB)Database-per-tenant
SecurityLogical, enforced in app and queriesPhysical, enforced by database boundary
ScalabilitySimple start, noisy-neighbor riskScale or move heavy tenants independently
OperationsLow to medium effortMedium to high: provisioning and migrations
CostCheapest, few moving partsHigher, needs automation and observability
BackupsWhole-database snapshotsPer-tenant backups and targeted restores
💡

Compliance or blast-radius concerns? Favor database-per-tenant for cleaner isolation and incident response

If compliance or tenant-level recovery matters, database-per-tenant wins. If speed and simplicity matter, start with row-level

Transition: Let’s anchor this with a Rails example

Rails example ERD

What you’ll learn: A minimal Rails entity-relationship diagram and two implementation options

A concrete anchor helps. Imagine multiple schools using one SaaS

  • Entities: School (tenant) - Course - Lesson
  • Goal: School A never sees School B’s courses or lessons
  • Routing: subdomain like acme.schoolapp.com maps to School “acme”

ERD overview

Here’s the entity relationship diagram showing how tenant isolation works:

erDiagram
    School ||--o{ Course : has_many
    Course ||--o{ Lesson : has_many

    School {
        int id PK
        string name
        string subdomain
        datetime created_at
        datetime updated_at
    }

    Course {
        int id PK
        int school_id FK
        string name
        datetime created_at
        datetime updated_at
    }

    Lesson {
        int id PK
        int course_id FK
        string title
        string content
        int position
        datetime created_at
        datetime updated_at
    }

Key isolation points:

  • School.subdomain enables routing (acme.schoolapp.com → School “acme”)
  • Course.school_id provides direct tenant filtering
  • Lesson inherits tenant context through Course relationship

Option A Row-level (single DB)

Short, cost-effective, and great for MVPs. Row-level means every query is scoped by the current tenant’s school_id

Migrations

# db/migrate/001_create_schools.rb
create_table :schools do |t|
  t.string :name, null: false
  t.string :subdomain, null: false, index: { unique: true }
  t.timestamps
end

# db/migrate/002_create_courses.rb
create_table :courses do |t|
  t.references :school, null: false, foreign_key: true, index: true
  t.string :name, null: false
  t.timestamps
end

# db/migrate/003_create_lessons.rb
create_table :lessons do |t|
  t.references :course, null: false, foreign_key: true, index: true
  t.string :title, null: false
  t.text :content
  t.integer :position
  t.timestamps
end

Current tenant and scoping

# app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
  attribute :school, :user
end

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :set_current_school

  private
  def set_current_school
    Current.school = School.find_by!(subdomain: request.subdomain)
  end
end

Models with guardrails

# app/models/concerns/multitenant.rb
module Multitenant
  extend ActiveSupport::Concern
  included do
    belongs_to :school
    validates :school, presence: true
    scope :for_current_school, -> { where(school_id: Current.school.id) }
    before_validation { self.school_id ||= Current.school&.id }
  end
end

# app/models/course.rb
class Course < ApplicationRecord
  include Multitenant
  has_many :lessons, dependent: :destroy
end

# app/models/lesson.rb
class Lesson < ApplicationRecord
  belongs_to :course
  has_one :school, through: :course
  validates :title, presence: true
end

Controller usage

class CoursesController < ApplicationController
  def index
    @courses = Course.for_current_school.order(:name)
  end

  def show
    @course = Course.for_current_school.find(params[:id])
  end
end

You keep one database, scope everything by Current.school, and write tests that assert tenant boundaries

Option B Database-per-tenant

More isolation and blast-radius control. The app switches database connections per request

Quick definitions:

  • Connection switching: selecting the tenant database for the duration of a request or job
  • Shard: a named database target for a tenant

Database config (high level)

# config/database.yml (excerpt)
development:
  primary:
    adapter: sqlite3
    database: db/development.sqlite3
  shards:
    acme:
      adapter: sqlite3
      database: db/tenants/acme.sqlite3
    beta:
      adapter: sqlite3
      database: db/tenants/beta.sqlite3

Switch per request

class ApplicationController < ActionController::Base
  around_action :switch_tenant

  private
  def switch_tenant
    school = School.find_by!(subdomain: request.subdomain)
    ActiveRecord::Base.connected_to(shard: school.subdomain) { yield }
  end
end

Background jobs carry the shard

class TenantJob < ApplicationJob
  queue_as :default
  def perform(subdomain)
    ActiveRecord::Base.connected_to(shard: subdomain) do
      # work safely within that school’s DB
    end
  end
end

You trade operational overhead for stronger isolation, per-tenant backups, and cleaner incident response

💡

Bake tenant context into jobs, caches, metrics, and logs so production triage stays tenant-aware

Transition: With patterns in place, prepare client-ready language

Client-ready answers

What you’ll learn: Clear statements you can paste into proposals and a checklist to size the work

One-liner: “Multitenancy is how a single app safely serves many customers by isolating each customer’s data and routing every request to the right tenant.”

Short paragraph: “When you say ‘multitenant,’ do you need true isolation or just users and roles? We can do row-level multitenancy with a tenant_id in one database for speed and cost, or database-per-tenant for stronger isolation and per-customer backups.”

Slightly technical: “Row-level multitenancy keeps one database and scopes queries with tenant_id. Database-per-tenant switches connections so each customer’s data lives in its own database. The trade-off is isolation versus operational complexity.”

Actionable steps before you estimate

  1. Define what “tenant” means in their domain, such as school, company, or brand
  2. Confirm routing needs, such as custom domain or subdomain, and single sign-on (SSO)
  3. Clarify compliance, data residency, and restore-per-tenant requirements
  4. Size the largest tenant and expected concurrency
  5. Pick the simplest pattern that meets isolation requirements

Quick picks that work

  • MVP with fewer than 100 tenants and low compliance risk: row-level multitenancy
  • Enterprise with per-customer SLAs or strong isolation needs: database-per-tenant
  • Unsure: start row-level and design seams to upgrade later

Remember this mental model: user management is not multitenancy; multitenancy decides where tenant data lives and how requests get there

💡

Keep it simple: start with the lightest pattern that meets isolation, bake tenant context into jobs and caches, and make upgrades a seam not a rewrite

📧