Skip to main content
Back to Blog
October 18, 2026 3 min read

One Codebase, Six Branded Sites

ArchitectureNext.jsMulti-tenant
One Codebase, Six Branded Sites

Real-estate developers each want their own branded sales website, and each one wants roughly the same things: a cinematic hero, a 3D walkthrough, a 360° tour of the show unit, a live availability list, and a lead form. The obvious way to deliver six of those is to build six websites. It is also the way that guarantees six codebases drifting apart, six places to fix the same bug, and a sixth client who waits because the team is busy with the fifth.

We built one application instead. An environment variable picks one entry out of a site config at build time, and that entry is the site.

The shape of it

The config entry carries a layout array — a list of section descriptors. The home page maps each descriptor's type through a SECTION_COMPONENTS lookup, renders them in order, and hands section.data straight to the component. That is the whole mechanism.

What it buys is specific and worth being precise about: adding a customer is one config entry and one data file. No routing work. No new pages. No copy-paste of a layout that then needs its own maintenance. Six variants ship from it today.

The thing to notice is that the sections are ordered data, not a template. A developer who wants the availability list above the tour instead of below it changes an array. They do not open a component, and they cannot accidentally change the other five sites while they are in there.

Where the config stops helping

Config-driven rendering has a well-known failure mode: the config grows into a programming language. You start with type and data, and a year later there is a condition field, then a variant, then something that is unmistakably an if-statement expressed in JSON. At that point you have invented a worse language and lost the tooling of the real one.

The line we held was that the config describes what appears and in what order, and never how it behaves. Behaviour lives in the component. When a client wanted something genuinely new, the answer was a new section component with a new type — a few dozen lines, testable on its own, invisible to the other five sites — not a new flag threaded through an existing one.

That rule has a cost. It means some near-duplicate components exist where a flag would have been shorter. I think that is the right trade. A flag on a shared component is a promise that two clients will keep wanting the same thing, and clients do not keep that promise.

The build-time decision

The variant is chosen at build time, not request time. That rules out serving all six from one deployment, which sounds like a loss until you look at what it buys: each site ships only its own content, its own images and its own routes. Nothing in the bundle belongs to a competitor's development, which matters more than it sounds when the clients are in the same market.

It also means a broken config fails the build rather than the page. A missing section type is a build error at 4pm on a Tuesday, not a blank panel a client finds at the weekend.

What I would keep

The pattern generalises past property sites. Any time you are about to build the same product for the nth customer with different content, the question worth asking first is whether the differences are content or behaviour. If they are content — and they usually are, far more than anyone expects at the start — then one application reading a config will beat n applications, and will keep beating them for as long as the config stays a description rather than a program.

Available for work

Let's create your next big idea.