Back to blog
Marcin Ostrowski  · Jul 13, 2026

Adding multi-tenancy to a live Rails app: what the gem guides skip

Retrofitting tenancy onto a product with real users is a different job than starting multi-tenant. Lessons from scaling one airport's system to all of Norway's biggest.

Every guide to adding multi-tenancy to an existing Rails app walks you through the same hour: install the gem, add the tenant model, scope the queries. All true, and all the easy part. The hard part is the word “existing.” Your app has live users, issued invoices, bookmarked URLs, and revenue arriving daily, and none of that pauses while you restructure the ground it stands on.

We’ve lived the harder version of this. FastTravel started as a booking system for one airport, with kiosks in the terminal and drivers depending on it for their day’s income. When the next airports wanted it, multi-tenant was the only sane architecture, and it had to arrive under a working product. Today the system runs every major airport in Norway on one codebase. Whether to make that move is its own decision, and I’ve written that post already. This one is about what the move involves when the app is already alive.

Greenfield tenancy is a pattern. Retrofit tenancy is a migration.

The difference is state. A greenfield app scopes everything from day one and never knows another way. A live app has years of data that belongs to nobody, in the formal sense: no tenant column, no tenant concept, just rows that all implicitly meant “the one customer we had.” Retrofitting means giving every one of those rows an owner, while they’re being read and written.

That’s the first piece of honest sizing: the work scales with your schema rather than the gem’s README. Every table needs the question “whose is this?”, and some tables answer it uncomfortably (shared lookup data, global settings that are about to stop being global, join tables whose two sides could theoretically belong to different tenants). The gem guides show you the happy table. Your codebase contains the other kind.

Some things are already promised to the outside world

A live app has made commitments. Invoices have been issued with numbers in a sequence; if numbering becomes per-tenant, the old sequence still has to make sense to an accountant in two years. URLs are bookmarked, embedded in emails, printed on receipts; whatever tenant routing you choose has to keep the old links resolving. Integrations authenticated against the app before tenancy existed and will keep calling the old way.

This is the category that genuinely separates retrofit from greenfield, because no refactoring tool sees it. The contract lives in your customers’ inboxes and your accountant’s files, outside your code entirely. Before any migration plan, the honest move is to write the list of what’s already promised, because each item on it is a constraint your tenancy design must absorb rather than break.

The order that keeps the product alive

The shape that works is the boring one: make the structure exist before any behavior depends on it. Tenant model and scoping first, with everything tenant-owned assigned to tenant number one, so the app runs exactly as before with one formal owner. The general Rails mechanics are well-trodden and worth following in order: add the tenant column nullable, backfill it in batches while live writes continue, only then add the NOT NULL constraint, and only then turn on default scoping. The leak detector is a synthetic second tenant in CI from day one, because the failure mode that matters (a query somebody forgot to scope, showing customer A’s data to customer B) is exactly the kind a test tenant catches and a code review skims past.

Then behavior changes ride normal releases, slice by slice, each individually revertable, the same discipline we used converting a unicorn’s views: no big-bang branch, no freeze, small enough that a bad slice is a small revert.

Tenant number two enters when the code has stopped being able to tell that tenant one is special, and a cheap way to check is to run the suite as a second tenant and grep for anything that assumes there’s only one.

Configuration is where the real work moved

On FastTravel, per-tenant configuration turned out to be the permanent dimension of the product, outlasting the tenancy machinery itself as a source of work. Each airport came with its own physical layout, its own rules, its own operational reality, down to its own hardware: barriers at the gates, chips in the vehicles, cameras above the lanes. That goes well past tenant_id into “what does this product even mean here?”, and it has to be answered as designed configuration data, or the codebase fills up with conditionals no one can reason about.

Billing diverges first and hardest. Fee structures, invoicing rules, corrections, automatic passage fees: Mateusz has run that payments module from day zero, and years later it’s still where continuous engineering happens, because every tenant stresses the money paths differently. If you want one early-warning system for whether your tenancy design holds, watch what happens to billing when tenant two arrives.

The metric that says you did it right

Tenant two is allowed to be an engineering project. The test is the slope after that, and on FastTravel each airport onboarding got more routine than the one before. The end state worth aiming at is the one where adding a tenant stops being an engineering event and becomes configuration plus go-live checks. That slope is the whole return on the retrofit investment: the difference between “we can take the next customer” being a sales answer or a quarter of roadmap.

If the slope refuses to flatten and every new tenant still needs custom code, the architecture isn’t truly multi-tenant yet, whatever the gems say, and the fix usually lives in the configuration layer rather than the scoping.

Start with the promise list

If you take one thing from a team that’s lived this: before the schema plan, write the list of what your app has already promised the outside world. Every constraint your tenancy design must absorb is on it, it costs an afternoon, and it’s the document that separates a retrofit that respects its users from one that breaks their bookmarks and invoices on migration day, and their trust with them.

fryga.io
fryga — a product engineering consultancy in Kraków, Poland. We build with companies in Spain, Norway, the US, and beyond.
Marcin writes about Rails and AI at rubyonai.com
The Rails AI harness, in the open at github.com/fryga-io/superpowers-rails
We're four people — maybe five: careers
[email protected]
RubyPL sp. z o. o.; ul. Będzińska 5 /8, 31-403 Kraków, Poland; KRS: 0001044822; NIP: 6762646011; REGON: 52575800600000