Serving Many Brands from One Codebase
How to serve many products or brands from a single codebase without forking — and why the real work is a boundary, not a theme.
A while ago, every new brand arrived as a request that sounded harmless: "can we have the same product, just ours?" The honest first answer is to copy the codebase, change the colours, and ship. It works the first time. It quietly stops working around the third.
By then you are not maintaining one product with a few variations. You are maintaining several near-identical products that have drifted apart, and a fix in one no longer reaches the others. The cost of "one more brand" turned out to be the cost of one more parallel codebase, forever.
So instead of forking, I started keeping a single codebase and treating everything that differs between brands as data, not code. That one decision is most of the story — and the hard part of it isn't what you'd expect.
The pattern: configuration over forks
The shape repeats anywhere one product is sold to many brands or tenants. The differences between them fall into a few kinds: how it looks, which capabilities are switched on, and a small amount of genuinely brand-specific behaviour. If each of those lives in code, every brand is a fork. If they live in configuration, a brand is set up rather than rebuilt.
In practice that means appearance comes from theme tokens — colours, type, spacing a brand supplies without touching components — capabilities are gated by feature flags, and the remaining specifics come from a per-brand configuration layer instead of branching logic sprinkled through the app. The product reads those definitions; it doesn't hardcode them.
Why teams get stuck
The trap is almost never technical. Theming is easy. The hard part is the boundary: deciding what becomes configuration and what stays custom code.
It's hard because every request looks reasonable on its own. Each brand has a legitimate reason for wanting something slightly different, so "just say no" isn't the answer. The real question is which differences represent a new platform capability that several brands will eventually want, and which are one-off exceptions that add complexity for everyone else forever.
Get that wrong in one direction and you drift back into forks — teams route around the platform and rebuild things locally. Get it wrong in the other direction and you over-configure: a configuration layer so flexible that it becomes an unreadable programming language nobody can reason about. An unpredictable config layer is just a slower way to write code.
The skill in a multi-brand platform isn't theming. It's the line between configuration and custom code — and defending it.
A better mental model
The move that makes this manageable is to stop reacting to requests literally and start looking for the shape behind them. Brand requests that look unrelated often collapse into the same capability: one brand wants a feature hidden, another wants it optional, a third wants a different default. Those are not three features — they are one configuration setting seen from three angles.
So when a request arrives, the useful question isn't "can we build this for them?" It's "is this a new setting the platform should own, or a single brand's exception?" If several brands would plausibly use it, it belongs in the platform. If only one ever will, it's an exception — and exceptions are a tax every other brand quietly pays.
The lesson
Serving many brands from one codebase is cheaper than forking, but only because someone keeps drawing a line. The configuration is the easy half; the judgment is the expensive half. Hold the boundary and new brands stay close to free — a setup, not a build. Let it slip and you end up with the forks you were trying to avoid, just hidden inside one repository instead of spread across several.