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
| Dimension | Row-level (single DB) | Database-per-tenant |
|---|---|---|
| Security | Logical, enforced in app and queries | Physical, enforced by database boundary |
| Scalability | Simple start, noisy-neighbor risk | Scale or move heavy tenants independently |
| Operations | Low to medium effort | Medium to high: provisioning and migrations |
| Cost | Cheapest, few moving parts | Higher, needs automation and observability |
| Backups | Whole-database snapshots | Per-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.subdomainenables routing (acme.schoolapp.com â School âacmeâ)Course.school_idprovides direct tenant filteringLessoninherits tenant context throughCourserelationship
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
- Define what âtenantâ means in their domain, such as school, company, or brand
- Confirm routing needs, such as custom domain or subdomain, and single sign-on (SSO)
- Clarify compliance, data residency, and restore-per-tenant requirements
- Size the largest tenant and expected concurrency
- 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