The deal is the biggest you've seen. The customer loves the product. There's one condition: it has to run under their brand, on their domain, looking like their software. Sales said yes before talking to engineering, because of course they did.
Now you're staring at a codebase that assumes one company, one logo, one domain, one set of config, all hardcoded because there was never a reason not to. The word "tenant" appears nowhere. And the realization lands: this isn't a feature. The single-tenant assumptions are baked into the data model, the auth flow, the frontend, and the deploy. Making the product white-label might mean rebuilding the spine of it.
It doesn't have to be a rewrite. But the decisions you make in the next two weeks determine whether it's a clean extension or a year of regret. If you want the longer version of how this plays out, our multi-tenant SaaS build is the same shape at scale.
First, separate the two things "white-label" actually means
Founders say "white-label" and mean one of two very different things, and conflating them is the first expensive mistake.
Branding is cosmetic: the customer's logo, colors, fonts, and a custom domain. The product is still obviously your product, just dressed in their brand. This is achievable as a configuration layer.
True white-label goes further: the customer's own users log in, the customer may resell to their customers, and your existence is invisible. This implies a reseller hierarchy, per-tenant user pools, and sometimes tenants-within-tenants.
These need different architectures. Build for branding when the deal needs reselling, and you're back at a rewrite in six months. Build the full reseller hierarchy for a deal that just wants a logo swap, and you've spent three months over-engineering. Pin down which one the deal actually requires before you design anything.
The decision underneath everything: tenant isolation
White-label is multi-tenancy with branding on top, so the load-bearing decision is how tenants are isolated in your data. Get this wrong and every other choice is built on sand. There are three models, and the right answer is usually the boring middle one.
Silo — a database (or schema) per tenant. Strongest isolation, easiest to reason about, and the answer when tenants demand their data physically separated for compliance or when a single enterprise customer is large enough to warrant it. The cost is operational: migrations now run across N databases, and onboarding a tenant is provisioning infrastructure, not inserting a row.
Pool — shared tables with a tenant_id on every row. Cheapest to operate, scales to many tenants, and the right default for most white-label SaaS. The risk is the entire ballgame: one missing WHERE tenant_id = ? leaks one customer's data into another's account, and in a white-label context that's two of their brands seeing each other. This risk is real but manageable, and the next section is how.
Bridge — shared infrastructure, separate schemas per tenant. A middle ground that buys logical separation without per-tenant databases. Useful when a few large tenants want isolation and a long tail can share.
Most teams should start pooled and silo the specific large customers who demand it. Designing the data model so a tenant can be promoted from pool to silo later is the foresight that prevents the rewrite.
Making the pool model safe: isolation that doesn't depend on discipline
The pooled model's danger is that correctness depends on every query remembering the tenant filter, and humans forget. The fix is to make the filter impossible to forget rather than asking everyone to be careful forever.
Enforce isolation at the data layer, not in application code. Postgres row-level security ties every query to the current tenant context, so a forgotten WHERE clause returns nothing instead of everything. The database becomes the backstop. Application-layer filtering alone means one careless query away from a breach, and you'll write thousands of queries.
Carry tenant context through the whole request, set once at the edge. Resolve the tenant from the incoming request — domain, subdomain, or token — and bind it to the request context immediately. Every downstream query reads tenant from context, never from a parameter a developer might forget to pass. One place sets it; everything else inherits it.
Test the breach, not just the feature. Your test suite needs a case that asserts tenant A literally cannot read tenant B's data through every access path. This is the test that fails loudly the day someone writes the query that would have leaked. Without it, you find out from a customer.
Theming and domains: the visible layer
Branding is where the customer experiences "white-label," and it's mostly a configuration problem once isolation is solved.
Theming is data, resolved per tenant, never a fork. The strongest mistake here is branching the frontend per customer — a tenant-acme/ folder. That path is a maintenance spiral where every feature ships N times. Instead, store branding as per-tenant config — logo URL, color tokens, font, copy overrides — and resolve it at render. CSS custom properties make this clean: the theme is a set of token values injected per tenant, and the same component tree renders every brand. One codebase, N appearances, resolved at runtime.
Custom domains are a real subsystem, not a CNAME. app.theircompany.com pointing at your platform sounds trivial and isn't. You need automated TLS certificate issuance and renewal per domain, a domain-to-tenant resolution map at the edge, and a verification flow proving the customer controls the domain before you serve their brand on it. Underestimate this and every new white-label customer is a manual, error-prone onboarding that breaks at certificate renewal.
Per-tenant configuration: the flag that becomes a fork if you let it
Once you have tenants, the requests start. This tenant wants a feature off. That one needs a different default. A reseller wants their own pricing. Each request is small; collectively they're how a clean platform rots into per-customer special cases.
The discipline: configuration is data with defaults, not code with conditionals. Define a config schema — feature flags, limits, defaults, integrations — with a platform-wide default and per-tenant overrides. A tenant's effective config is the default with their overrides applied. When a tenant wants a feature off, you flip a flag in their config, not write if tenant == 'acme' in the product code. The moment a tenant's name appears in a conditional, you've started the fork that ends in N codebases.
What to design for upfront, even before you need it
You won't build everything on day one, but a few seams must exist from the start or retrofitting them is the rewrite you're trying to avoid:
tenant_idon every domain row, from the first migration. Adding a tenant column to a populated table later is a data migration on live data — the exact pain this whole exercise exists to dodge.- Tenant resolved at the request edge. Even if there's one tenant today, route requests through tenant resolution so the rest of the system already speaks "tenant" before there's a second one.
- Branding read from config, never hardcoded. Even your own brand should load through the theming layer, so your product is tenant zero and adding tenant one is a row, not a refactor.
- A path from pool to silo. Model data so a single large tenant can be migrated to isolated storage without reshaping the schema.
What fixed looks like
Onboarding a white-label customer is filling out a config record — brand tokens, a verified domain, feature overrides — not a branch, a deploy, or a provisioning project. The same component tree renders their brand because theming is resolved data, not a fork.
Tenant isolation is enforced at the database, so a forgotten filter returns nothing instead of someone else's data, and a test asserts that boundary holds across every access path. Custom domains issue and renew certificates without anyone touching them.
Per-tenant requests are config flips, so the codebase stays single even as the brands multiply. And the one enterprise customer who demands physically separated data gets siloed without reshaping the schema, because the path was designed in from the start.
./tenant --isolate=rls --theme=tokens --config=overrides --domains=auto-tls
This is for you if
You're a funded SaaS with a single-tenant codebase and a white-label deal — or three — on the table, and you can feel that saying yes naively means a rewrite. This is a $50k+ engagement: establishing tenant isolation, the theming and config layers, and custom-domain handling so the next white-label customer is a config record, not a project. For platforms heading toward a reseller model — tenants who resell to their own customers, hierarchical user pools, per-reseller billing — it's the $100k+ version, where the multi-tenancy decisions get genuinely hard and getting them right early is the difference between a platform and a pile of forks.
It's the right time if a deal is contingent on white-label, if your data model has no concept of a tenant, or if you're about to write the first if customer == conditional that becomes the fork.
This is not for you if you have one customer, no reseller path, and white-label is a hypothetical on a roadmap. Hardcode your brand, ship the product, and revisit when a real deal puts money behind the requirement. Building tenant infrastructure before a tenant who needs it is paying for a problem that hasn't arrived.
< transmit >