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:
- Silent cross-tenant leaks — a report endpoint that aggregated across all vehicles because nobody had scoped the queryset. Nothing crashed. It just returned the wrong numbers to the wrong customer.
- Singleton assumptions in caching — cache keys like
pricing_configinstead ofpricing_config:{tenant_id}. Works perfectly with one tenant, silently corrupts data with two. - Background jobs with implicit scope — a nightly billing job that looped over "all active rentals" and had no concept that "all" now meant "all, across every customer, with different billing rules."
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:
- We added tenant scoping everywhere before we onboarded a second real tenant, using a synthetic second tenant in staging to catch leaks early.
- 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.
- 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.