Building a multi-tenant SaaS from MVP â 6 mistakes you must not make
Every SaaS you launch turns multi-tenant sooner or later. The only question is whether you plan for it, or your 7th customer forces it on you in a late-night incident. Six mistakes we or our clients have made â and want to spare you from.
1. Forgetting tenant_id on a table
The ground rule: every business table's first column is tenant_id. Even tags. Even user_preferences. The smallest exception comes back to bite you â one client of ours discovered 14 months in that email_templates had no tenant_id, and a template saved by tenant B showed up in tenant A's account.
Enforce it at migration level: no table without tenant_id unless explicitly shared (e.g. tenants, tenant_billing_plans).
2. Launching without row-level security (RLS)
âThe service layer will check.â It won't. One forgotten WHERE in an admin tool, one badly scoped JOIN, and data leaks sideways. Turning on Postgres RLS isn't optional â it goes in during the MVP's first week.
A measured datapoint from a 2026 client audit: 3 out of 17 service-layer queries didn't filter on tenant_id. With RLS those errored out; without it, they leaked silently.
3. Sharding too early
Despite the âwe're going to scaleâ narrative: one Postgres is enough for the first 200 tenants. The sharding tax (cross-shard JOINs, eventual consistency, devops complexity) is more cost than benefit right now. Add the shard-key column (shard_key), add the tenantâshard mapping table â but stay on one shard until p99 > 500ms or DB-CPU > 70% becomes sustained.
4. Shared sequence across tenants
quote_number and friends: if you use SERIAL with one global sequence, tenant B's quote numbers jump whenever tenant A inserts. That's not just ugly â it breeds accounting issues.
Pattern: composite (tenant_id, sequence_id), INSERT ... RETURNING from a per-tenant nextval('tenant_X_quotes_seq'), or an explicit tenant_counters table with FOR UPDATE. Simple, and both parties stay sane.
5. Hard-coded subdomain
âWe'll put Acme under acme.app.example.com for now.â Six months in: 47 subdomains, an unmaintainable Cloudflare DNS ruleset, SSL cert rotation is a nightmare. And the new customer already wants the SaaS on their own domain (portal.acme.com).
From day one: Host-header based tenant resolution. The subdomain is just a convenience alias. In the DB, the tenant's host_aliases field holds a list (acme.com, portal.acme.com, acme.app.example.com). One ingress, one wildcard cert, many hosts.
6. Shared search index
Elasticsearch / Meilisearch / OpenSearch: if the whole SaaS searches a single index, the tenant_id filter is only at query time. The query planner gets expensive, re-indexing one tenant touches the whole index, and one config error leads to data leakage again. Tenant-per-index (or at least per-index-alias) â slightly pricier storage, vastly simpler security.
Takeaway
Multi-tenancy isn't a switch you flip later. It's an architectural layer you have to design in on day one of the MVP. Every one of the six mistakes above demands a rewrite at month 18 â unless you got it right at month one.