stacknqueue
← All writing

What Actually Breaks When You Rebuild for Multi-Tenancy

June 2, 2026

When we started building our fleet management platform, it had exactly one customer. Every model, every cron job, every cache key was written with that assumption baked in, quietly, without anyone deciding it on purpose. By the time we needed to support 10+ locations, that assumption had spread into places we didn't expect.

The obvious part: adding a tenant column

Adding tenant_id to the core models was the easy 20%. Django makes this mechanically simple — a foreign key, a migration, done. The harder part was finding every query that should have been scoped to a tenant but wasn't, because it had never needed to be.

We found three categories of bugs:

None of these show up in a code review that isn't specifically hunting for them. They show up in production, with a customer on the phone.

Schema-per-tenant vs. shared schema

We went with a shared schema and a tenant_id discriminator rather than schema-per-tenant or database-per-tenant. For our scale (10+ locations, not 10,000), the operational simplicity of one schema — one set of migrations, one connection pool, one thing to back up — outweighed the stronger isolation guarantees of separate schemas. If we were building a platform for thousands of tenants with wildly different data volumes, I'd make a different call. At our scale, the failure mode we needed to design against was a bug leaking data across tenants, not one tenant's load starving another's — so row-level scoping plus disciplined query review solved the actual problem.

What made the rollout survivable

The part that saved us wasn't clever code, it was sequencing:

  1. We added tenant scoping everywhere before we onboarded a second real tenant, using a synthetic second tenant in staging to catch leaks early.
  2. We rolled out new locations one at a time, not in a batch, so a scoping bug affected one customer instead of all of them.
  3. We wrote a small test helper that asserted "no queryset touches this table without a tenant filter" for the models that mattered most, and ran it in CI.

The rewrite took longer than the initial estimate — multi-tenancy work always does, because the cost isn't in the schema change, it's in finding every place that quietly depended on there being only one tenant. If you're heading into a similar rebuild, budget time for that hunt specifically, separate from the "add the column" work. That's where the real risk lives.