---
title: "When should your Rails app go multi-tenant? A decision framework for CTOs"
date: "2026-06-22"
excerpt: "Multi-tenancy is a business decision wearing an architecture costume. A framework for when to make the move, which isolation level to pick, and what it really costs."
author: "Marcin Ostrowski"
---

Every Rails multi-tenancy article I can find starts at the gem install. Which is strange, because by the time you're choosing between acts_as_tenant and schema separation, the expensive decision is already behind you, and nobody wrote about that one.

I got to make the expensive decision on a real product. FastTravel started as a booking system for one airport: kiosks, a driver app, payments. Then the next airports wanted it, and each came with its own physical layout, its own payment rules, and its own operational reality. Today it runs every major airport in Norway on one codebase, no forks. Going multi-tenant was the engineering effort of that scaling story, and most of what I believe about the decision comes from it.

## Multi-tenancy is a business decision in an architecture costume

Strip the terminology and the question is: will customer number two differ from customer number one in data and configuration, or in product? Those are different situations, and only one of them is multi-tenancy.

If the next customer needs the same product with their own data, their own settings, their own users walled off from everyone else's, that's a tenant. If the next customer needs different features, different workflows, a different product that happens to share your codebase's DNA, that's not a tenant, and multi-tenancy will not save you from the fork you're about to create.

The common case sits between: customer two is 90% tenant plus one bespoke demand. The answer there is per-tenant entitlements (features switched on and off as tenant configuration), or the word no. The moment a customer's special feature becomes a branch instead of a flag, you've left multi-tenancy without noticing.

At FastTravel the second airport was unambiguously a tenant. Same product: book a taxi, manage the queue, bill the drivers. Different everything else: layout, pricing rules, regional requirements. That's the clean case. When you have it, the architecture question becomes when, not whether.

## When "not yet" is the right answer

With one paying tenant and a pipeline of hypothetical ones, multi-tenancy is a tax on everything you build, paid in advance, for customers who may never arrive. Every feature now needs the question "per-tenant or global?", every query needs scoping, every test needs a second tenant in the fixtures. Sales decks don't justify that tax; a signed second customer does. One caveat keeps the rule honest: if you already know you're selling B2B, a tenant id column from day one is cheap insurance, because retrofitting tenancy onto live customer data later is real work done under a sales deadline. The rule is about when to invest in the tenancy infrastructure, not when to add the column.

The opposite mistake costs more, though, and it's sneakier: serving customer number two by deploying a second copy of the app. One copy becomes three, three become seven, and now every migration runs N times, every hotfix ships N times, configuration drifts between copies, and your team spends its week doing deployment archaeology instead of product work. Deploy-per-customer feels cheap because each step is small. The total is how products quietly die. If the customers differ in data and config, not in product, the N-deployments path is the expensive one even though it looks free today.

So the timing rule I'd defend: go multi-tenant when the second real customer signs, and not a feature earlier.

## The three isolation levels, and how to pick

Rails gives you three broad ways to keep tenants apart, and the trade is always isolation versus operational cost.

Row-level isolation (a tenant_id on every table, the acts_as_tenant approach) is the cheapest to run and the easiest to migrate to: one schema, one migration per change, tenants as data. Its weakness is that isolation is only as strong as your scoping discipline, and one missing scope is a data leak between customers. Postgres row-level security exists as the belt-and-braces answer to exactly that risk. This is where most SaaS products should start, with the discipline enforced by defaults and tests rather than hope, and the gem situation is simple: acts_as_tenant is actively maintained and current.

Schema-level isolation (one PostgreSQL schema per tenant, the approach the apartment gem made popular) buys harder separation, and pays for it at migration time: a hundred tenants means a hundred schema migrations per change, and the gem ecosystem around this approach has a history of maintenance gaps. It earns its cost when tenants demand stronger separation than rows but you're not ready to run separate databases. If you go this way, know that the original apartment gem was abandoned years ago; the maintained fork is ros-apartment, and that history is itself a data point about how many teams stay on this road.

Database-per-tenant is the strongest wall and the highest operational bill: provisioning, backups, connection management, and cross-tenant reporting all become real engineering. You usually don't choose this one. It gets chosen for you, by compliance requirements or by an enterprise contract with a data-residency clause.

The selector, compressed: start row-level unless a regulator, a contract, or a security model forces you up a level. And whichever you pick, pick it before tenant two ships, because moving between isolation levels with live customers is the genuinely painful version of this work.

## The costs nobody puts in the comparison table

The gem choice turns out to be the small line item. What actually consumed our engineering at FastTravel, and what I'd budget for anywhere:

Per-tenant configuration becomes a product of its own. Each airport had its own layout, rules, and pricing. The temptation is an `if tenant == ...` here and there, and that way lies a codebase where behavior is unknowable. Configuration has to be data, with a schema and an owner, or every new tenant adds a layer of conditional sediment.

Billing diverges before anything else does. Different tenants want different fee structures, different invoicing, different corrections. The payments module was a from-day-zero concern at FastTravel and it's the part that still gets continuous engineering years later. Multi-tenant billing deserves its own post.

Onboarding is the metric that tells you if you got it right. Tenant number two is allowed to be an engineering project. Tenant number five should be configuration plus go-live checks, not code. If every new tenant still needs a developer, you built N products that share a repo, and the repo is the only thing that's multi-tenant.

## What it bought

The reason to pay any of this: when the next airports asked, the answer was yes without a rewrite, and FastTravel went from one airport to every major one in the country on a single codebase, with tenant onboarding getting more routine each time. Multi-tenancy done at the right time is what turns "we'd like it too" from an engineering crisis into a sales conversation. That conversion rate, not the gem choice, is what the architecture is for.
