35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English 35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English
Multi-Tenancy in Rails: Drie Benaderingen en Wanneer Ze Vastlopen

Multi-Tenancy in Rails: Drie Benaderingen en Wanneer Ze Vastlopen

TTB Software
rails, architecture
Scoped queries, schema-per-tenant of database-per-tenant? Een praktische vergelijking van Rails multi-tenancy benaderingen met hun breekpunten.

Je bouwt een SaaS-product. Meerdere klanten delen je applicatie. Vroeg of laat stelt iemand de vraag die meer architectuurmeetings heeft ontspoord dan de tabs-versus-spaties-discussie: hoe houden we hun data gescheiden?

Rails geeft je genoeg touw om multi-tenancy op verschillende manieren te implementeren. De uitdaging zit niet in het kiezen — maar in begrijpen waar elke aanpak begint te kraken onder belasting, en hoe pijnlijk de migratie wordt als je moet switchen.

Aanpak 1: Scoped Queries met een tenant_id Kolom

Het meest voorkomende startpunt. Elke tabel krijgt een tenant_id kolom, en elke query wordt gescoped.

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true

  belongs_to :tenant

  default_scope { where(tenant_id: Current.tenant_id) }
end

Veel teams grijpen hier naar default_scope. Het werkt totdat het niet meer werkt. Het probleem met default_scope is dat het onzichtbaar is — zes maanden later schrijft een developer een rapportagequery en vergeet dat die naar de data van één tenant kijkt. Of erger: ze unscoped-en zich een weg naar een datalek.

Een explicieter patroon gebruikt ActsAsTenant of een handmatige concern:

module Tenanted
  extend ActiveSupport::Concern

  included do
    belongs_to :tenant
    validates :tenant_id, presence: true

    scope :for_tenant, ->(tenant) { where(tenant: tenant) }
  end
end

En in je controller-laag:

class ApplicationController < ActionController::Base
  before_action :set_current_tenant

  private

  def set_current_tenant
    Current.tenant = Tenant.find_by!(
      subdomain: request.subdomain
    )
  end
end

Waar het vastloopt: Rond 50-100 tenants met sterk verschillende datavolumes. Je grootste klant heeft 40 miljoen rijen in orders. Je kleinste heeft er 200. Dezelfde indexen bedienen beide, maar de query planner maakt heel andere keuzes op die schalen. Je eindigt met tenant-specifieke query-optimalisaties, wat de eenvoud tenietdoet die je hier naartoe trok.

Het echte pijnpunt zijn migraties. Zero-downtime migratietechnieken toepassen helpt, maar een index toevoegen op een tabel met 200 miljoen rijen blokkeert writes. Je kleine tenants hebben die index helemaal niet nodig. Je optimaliseert voor iedereen en niemand tegelijk.

Aanpak 2: Schema-per-Tenant (PostgreSQL Schemas)

PostgreSQL-schemas laten je tabellen namespaced houden binnen één database. Elke tenant krijgt een eigen schema met identieke tabelstructuren. De apartment gem maakte dit populair in Rails:

# config/initializers/apartment.rb
Apartment.configure do |config|
  config.excluded_models = %w[Tenant]
  config.tenant_names = -> { Tenant.pluck(:subdomain) }
end

Wisselen tussen tenants wordt een schema-switch:

Apartment::Tenant.switch!('acme_corp')
# Alle queries gaan nu naar het acme_corp schema

Dit is oprecht elegant voor kleine tot middelgrote aantallen tenants. De data van elke tenant is fysiek gescheiden, dus er is geen risico op cross-tenant lekkage door een missende WHERE clause. Backups en data-exports per tenant worden eenvoudig — dump één schema.

Waar het vastloopt: Migraties. Elke rails db:migrate draait tegen elk schema sequentieel. Met 500 tenants duurt een simpele add_column minuten. Met 5.000 tenants kijk je naar uren. De apartment gem handelt dit af, maar de operationele realiteit is pijnlijk.

Connection pooling wordt ook ingewikkeld. PostgreSQL’s search_path is connection-level state. Als je PgBouncer in transaction mode gebruikt (en dat zou je moeten doen op schaal), botst schema-switching per-request met connection sharing. Je hebt dan session mode nodig voor tenant-switching connections, wat je pool-efficiëntie beperkt.

Er is ook het plafond op totaal aantal objecten per database. PostgreSQL handelt duizenden schemas prima af, maar monitoring, vacuuming en pg_stat_user_tables worden allemaal trager naarmate je catalogus groeit.

Aanpak 3: Database-per-Tenant

Elke tenant krijgt een eigen database. Volledige isolatie. In Rails 6+ kun je de ingebouwde multi-database ondersteuning gebruiken:

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true

  connects_to shards: {
    default: { writing: :primary, reading: :primary_replica },
    tenant_a: { writing: :tenant_a, reading: :tenant_a_replica },
    tenant_b: { writing: :tenant_b, reading: :tenant_b_replica }
  }
end

Of dynamisch, wat praktischer is:

module TenantConnection
  def self.switch(tenant)
    config = tenant.database_config
    ActiveRecord::Base.establish_connection(config)
    yield
  ensure
    ActiveRecord::Base.establish_connection(:primary)
  end
end

De isolatie is echt. Een ontspoorde query van één tenant kan andere niet uithongeren. Je kunt high-value tenants op dedicated hardware zetten. Compliance wordt eenvoudiger — “jouw data staat in deze specifieke database in deze specifieke regio.”

Waar het vastloopt: Operationele complexiteit schaalt lineair met het aantal tenants. Elke database heeft een eigen backup-schema nodig, eigen monitoring, een eigen connection pool. Migraties vereisen orkestratie — je hebt tooling nodig om migraties over honderden databases te draaien, failures halverwege af te handelen, en bij te houden welke databases op welke schemaversie zitten.

Cross-tenant rapportage is ook ellendig. “Hoeveel totale gebruikers hebben we?” vereist dat je elke database bevraagt. Bouw vroeg een aparte analytics-pipeline, anders krijg je er spijt van.

Het Besliskader

Zo adviseer ik teams als fractional CTO:

Begin met scoped queries als je minder dan 100 tenants hebt met vergelijkbare datavolumes, je team klein is, en je snel moet shippen. De acts_as_tenant gem voegt vangrails toe zonder de complexiteit van schemabeheer.

Stap over naar schema-per-tenant als je sterkere isolatiegaranties nodig hebt, je tenant-count onder een paar duizend blijft, en je op PostgreSQL zit. De migratie-overhead is beheersbaar met tooling zoals apartment-sidekiq voor parallelle migraties.

Ga voor database-per-tenant als je compliance-vereisten hebt (data residency, SOC 2 met tenant-isolatie), tenants met dramatisch verschillende performance-profielen, of enterprise-klanten die het contractueel eisen.

De Hybride Waar Niemand Over Praat

De meeste volwassen SaaS-platformen eindigen hybride. Kleine tenants delen een database met scoped queries. Mid-tier tenants krijgen dedicated schemas. Enterprise tenants krijgen dedicated databases. De routeringslaag beslist op basis van tenant-tier:

class TenantRouter
  def self.connect(tenant)
    case tenant.tier
    when 'enterprise'
      establish_dedicated_connection(tenant)
    when 'professional'
      switch_schema(tenant)
    else
      set_tenant_scope(tenant)
    end
  end
end

Dit is meer code om te onderhouden, maar het matcht de economische realiteit: je kunt geen dedicated infrastructuur betalen voor free-tier tenants, en je kunt je grootste klant niet op gedeelde tabellen zetten met een WHERE clause als enige bescherming.

Migreren Tussen Benaderingen

Het migratiepad is belangrijker dan het startpunt. Van scoped queries naar schemas verhuizen is relatief pijnloos — maak schemas aan, kopieer data, update de routing. Van schemas naar aparte databases is lastiger maar haalbaar.

De andere kant op bewegen — aparte databases consolideren naar gedeelde tabellen — is een nachtmerrie. Je merged data, lost ID-conflicten op, en herschrijft queries. Ik heb het één keer gedaan. Ik zou het niet aanraden.

Kies de eenvoudigste aanpak die je volgende 18 maanden aankan. Bouw de abstractielaag nu zodat de tenant-resolutielogica op één plek zit. Als het moment komt om te migreren, wijzig je de router, niet elke query in je applicatie.

De teams die multi-tenancy goed aanpakken zijn niet degenen die op dag één de perfecte architectuur kozen. Het zijn degenen die de switch eenvoudig maakten toen de requirements veranderden. Als je twijfelt welke aanpak bij jouw fase past, kan een fractional CTO je helpen die keuze te maken op basis van je werkelijke data en groeitraject.

Veelgestelde Vragen

Welke multi-tenancy aanpak is het beste voor een nieuwe Rails SaaS?

Begin met scoped queries via de acts_as_tenant gem. Het is het simpelst te implementeren, vereist geen speciale databaseconfiguratie, en werkt goed voor de meeste apps tot je 50-100 tenants bereikt met sterk verschillende datavolumes. Je kunt later altijd migreren naar schema- of database-isolatie als dat nodig is.

Hoe voorkom ik datalekken tussen tenants?

Gebruik de acts_as_tenant gem of een custom concern die tenant-scoping afdwingt op model-niveau — vertrouw nooit op handmatige WHERE clauses in elke query. Schrijf integratietests die data voor twee tenants aanmaken en verifieer dat queries voor de ene tenant nooit data van de andere retourneren. Schema-per-tenant en database-per-tenant bieden sterkere isolatie by design.

Kan ik multi-tenancy benaderingen mixen in dezelfde applicatie?

Ja, en de meeste volwassen SaaS-platformen doen precies dit. Kleine tenants delen tabellen met scoped queries, mid-tier tenants krijgen dedicated schemas, en enterprise tenants krijgen geïsoleerde databases. De sleutel is vroeg een tenant router-abstractie bouwen zodat de switch-logica op één plek zit.

Hoe beïnvloedt multi-tenancy Rails caching?

Elke cache key moet de tenant-identifier bevatten, anders serveer je de gecachte data van de ene tenant aan de andere. Met scoped queries neem je Current.tenant_id op in je cache keys. Met schema-per-tenant handelt de schema-switch het impliciet af. Test cache-isolatie altijd expliciet in je testsuite.

T

About the Author

Roger Heykoop is een senior Ruby on Rails ontwikkelaar met 19+ jaar Rails ervaring en 35+ jaar ervaring in softwareontwikkeling. Hij is gespecialiseerd in Rails modernisering, performance optimalisatie, en AI-ondersteunde ontwikkeling.

Get in Touch

Share this article

Need Expert Rails Development?

Let's discuss how we can help you build or modernize your Rails application with 19+ years of expertise

Schedule a Free Consultation