We built a multi-tenant asset management platform over four months with a three-person team — one Django engineer, one React engineer, one React Native engineer. No dedicated infra role. The product tracks physical assets, maintenance schedules, and issues across an organization, with a web admin app and a field app for scanning and updating assets on site.
Multi-tenancy was baked in from day one, which is the right instinct for a SaaS product. The implementation is where we got it wrong. We gave every tenant a separate PostgreSQL schema — full isolation, clean separation, a tenant-routing layer that resolved which schema to query based on the authenticated user's organization. On paper it's the safe choice. In practice, the client's actual usage was a small number of tenants with modest data volumes and no isolation requirement — no regulation, no contract clause, nothing — that justified the complexity we'd already committed to.
That mismatch became the defining technical theme of the project.
The cost shows up in the support phase, not the build phase
A single model change stops being one migration and becomes N migrations — one per tenant schema, with N growing every time a new tenant signs up. Migration failures get harder to reason about: a schema migration can succeed on some tenants and fail partway through others, leaving the system in an inconsistent state that's genuinely painful to recover from. Onboarding a new tenant stops being a data insert and becomes an operational task — provision a schema, run the migration set against it, verify it landed clean. Anything that needs to look across tenants at once — cross-tenant reporting, admin analytics — has to work around data that's physically partitioned instead of just querying a table. And every request carries connection and search-path bookkeeping that a shared-schema design wouldn't need at all.
None of this is theoretical. It's the daily tax the team pays on every feature and every migration, for isolation the business never asked for.
Why we haven't migrated off it
Moving a live system with real tenant data off schema-per-tenant touches the data layer, the tenant-routing logic, and every query in the application. It's a genuinely risky undertaking, and the pragmatic call was to keep managing the existing architecture rather than take on that risk mid-flight. That's the real lesson here, more than the schema choice itself: an early architecture decision that's hard to reverse becomes a constraint you carry for the life of the product — even once everyone agrees it was the wrong call.
The framework we'd apply now
Multi-tenancy isn't binary. It's a spectrum — shared tables with a tenant_id column and disciplined query scoping at one end, schema-per-tenant in the middle, database-per-tenant at the other — and each step up that spectrum buys isolation at a real, growing operational cost. The question that should decide where you land isn't "which option feels safest." It's: what specific regulatory, contractual, or scale requirement forces this level of isolation? If you can't name one, you default to the cheapest tier that works, and you escalate only when a real requirement shows up — not in anticipation of one.
What we'd build differently
Regardless of which isolation tier you pick, tenant-scoping belongs in the data access layer centrally — every query tenant-scoped by default through one enforced mechanism, so isolation doesn't depend on every developer remembering to filter correctly. That decision is independent of the schema-vs-shared-table question, and we'd make it on every multi-tenant build going forward regardless of which isolation tier we chose.
The asset management product itself is solid — the core registry, maintenance scheduling, and issue tracking cover the operational need well, and the field app is where it earns its keep day to day. The architecture underneath it is the part we'd redo.
Frequently Asked Questions
When should a SaaS product use schema-per-tenant instead of a shared table with tenant_id?
Only when a regulatory, contractual, or scale requirement explicitly demands hard isolation. Absent that, shared tables with row-level tenant scoping deliver equivalent practical isolation with far less operational cost.
What's the actual cost of schema-per-tenant multi-tenancy?
Every schema migration multiplies by tenant count, onboarding becomes a provisioning task instead of a data insert, and cross-tenant reporting has to work around physical data partitioning rather than a single query.
Can you migrate from schema-per-tenant back to shared-schema later?
Technically yes, but it touches the data layer, tenant routing, and every query in the app on a system with live data — which is why most teams that over-provisioned isolation end up living with it instead of migrating off it.
How do you decide the right multi-tenancy isolation level for a new SaaS product?
Start from the lightest tier — shared tables, tenant-scoped queries — and only escalate to schema- or database-per-tenant when a specific, named requirement forces it. Never as a default "just in case" choice.