Engineering Deep Dive

One Deployment, a Whole Brand Family

How one Next.js deployment serves a family of branded storefronts while they migrate off legacy PHP: the proxy maps each domain into brand and theme route segments, ISR keeps thousands of pages fast and fresh, and one revalidate hook bridges both generations during the cutover.

Updated September 2026 ยท About a 7 minute read

Private employer Details are sanitized; architecture and outcomes are described without proprietary identifiers.

TL;DR

  • A family of branded PHP storefronts became one Next.js deployment where the brand is data: domain and theme are route segments resolved before any page code runs.
  • ISR with brand- and type-scoped cache tags keeps thousands of pages fast off a DB2 backend, and one revalidate hook invalidates the new app and clears the legacy cache during the migration.
  • Adding or restyling a brand is a registry entry, a content payload, and a theme choice, and the two generations coexisted without seams while sites moved one at a time.

Context

The business runs a family of niche branded storefronts, each one a separate domain aimed at a specific product category, each with its own logo, palette, and curated catalog. They began life as a set of PHP sites, one template lineage copied and tweaked per brand, and they carried the usual cost of that shape: a fix made on one site was a fix owed to all the others, and the newest storefront was always the only fully healthy one.

The rebuild replaces that fleet with a single Next.js application. The premise is that these sites are roughly identical underneath and differ only in brand, so the brand should be data and the code should be one deployment. The catch is that the rebuild cannot happen all at once: the sites move over one at a time, so for the entire migration the new Next.js application and the legacy PHP sites have to run side by side, serving the same brand family, sharing the same back office, and staying consistent with each other.

So this is really two engineering problems wearing one project. The first is multi-tenancy: one deployment that renders any brand fast and indexable. The second is a live migration: swapping storefronts out from under a running fleet without the half-migrated state showing seams to customers or to search engines.

Constraints

  • One team, a whole fleet. Every mechanism that scales per storefront gets paid once per brand. Standing up or restyling a site had to be data and configuration, not a new codebase or a new deploy.
  • Fast and indexable, from a legacy backend. Product and category pages number in the thousands across brands, search engines need fully rendered markup, and the catalog is an IBM DB2 system reached over ODBC. Putting that round trip on every page view was never an option.
  • Two generations, live at once. Throughout the migration some brands serve from the new app and some from the old PHP sites. Content edits and cache clears have to reach whichever generation currently owns a given brand, with no daily window where the two disagree.
  • Old links must keep their equity. Years of bookmarks and indexed URLs use the legacy PHP query conventions. Each one is SEO value the new app had to catch and translate, not answer with a 404.

Options considered

Option A: one deployment per storefront. Keep forking or templating per brand. It is the model the PHP generation already disproved for this team: a fix rolls out once per site, the sites drift, and maintenance grows with the catalog of brands. Rejected as the thing being migrated away from.

Option B: render every request live. One multi-tenant app that queries DB2 on every page view makes freshness trivially correct and makes the legacy database the bottleneck for all storefront traffic, crawlers included. Rejected on performance and on protecting a system the rest of the business also leans on.

Option C: static-export the fleet on a schedule. Pre-build every brand's pages into static files. Fastest possible pages, but build cost scales with brands times pages, and a single banner change waits for the next full build. A one-line content edit should not rebuild an entire fleet of websites. Rejected on freshness and build economics.

The chosen design takes the single deployment from A and the static speed from C, then buys back freshness with Incremental Static Regeneration: prerender each brand, revalidate on a timer, and invalidate precisely on demand when someone actually changes something. The same on-demand invalidation hook is what makes the two-generation coexistence work.

The design

Tenancy is resolved into the route before any page code runs. Each brand's domain maps onto a dynamic route segment, and the brand's visual style is a second segment naming one of a small set of interchangeable themes that all draw from one shared component library. Development addresses a brand through the full explicit path; production collapses the same structure to the domain root through the proxy layer, so the identical page code serves both without knowing which. At build time the app enumerates the live brands from the backend registry to generate their static params, and it refuses any host that is not in that registry rather than falling through to an accidental default store.

Both the catalog and the editable brand content come from the one DB2 backend over a pooled ODBC connection, but in two different shapes. Categories and products are relational reads; each brand's chrome, its logo, headings, carousels, and menu, is a single JSON content payload fetched by section key and rendered through the theme. Every data call goes through one fetch wrapper that standardizes its cache policy, and that wrapper exposes exactly three modes: a timed revalidation with cache tags for catalog data, an on-demand-only mode keyed purely by tags for content that changes rarely but must change instantly, and a no-cache escape hatch. Cache tags are scoped both by brand and by content type, so an edit can invalidate one brand's content without disturbing another's or the shared catalog.

Shopper requestany brand domainProxyhost becomes brand + themeDynamic routeregistry-gated brandsPrerendered pagesISR static cacheregenerateDB2 backendcatalog rows + JSON contentBack-office editsecret-guarded revalidate hookinvalidate ISR tagLegacy PHP site cachebridge, dated for removal

One deployment serving the whole brand family. Use "Next step" to walk a request through it.

A shopper request to any brand domain is mapped by the proxy into a domain-and-theme route, served from the ISR static cache. Expired pages regenerate in the background from the one DB2 backend, reading relational catalog and per-brand JSON content. A secret-guarded revalidate hook invalidates cache tags on demand, and during the migration it also clears the legacy PHP file cache for the same brand, so both generations stay consistent.

The freshness loop closes in two ways, and the second is where the migration lives. Timed revalidation lets pages age out on windows suited to their data, serving the stale page while the replacement builds in the background so no visitor ever waits on regeneration. On-demand revalidation is a single secret-guarded endpoint that takes a tag and invalidates exactly the matching pages. During the migration that same endpoint does double duty: for a given brand it invalidates the new app's cache tags and clears the legacy PHP site's file cache in the same call, so an edit made in the shared back office lands on whichever generation currently serves that brand. It carries an explicit note to delete the legacy half once the last site moves over, which makes it a temporary bridge by design rather than permanent coupling. Legacy URLs get their own translator in the proxy: it recognizes the old PHP query conventions and redirects them into the new structure, behind a matcher that skips static assets so the translation cost is paid only where it can matter.

What happened

One deployment now serves the migrated brands: thousands of prerendered product and category pages, sub-second loads, and server-rendered markup that held search visibility across the cutover. Standing up or restyling a brand became a registry entry, a content payload, and a theme choice instead of a codebase, and back-office edits reach the storefronts through the revalidate hook without a developer or a deploy in the loop.

The honest tradeoffs are the ones this shape always carries, plus one the migration adds. A single deployment is a single blast radius: a bad release touches every migrated brand at once, which is why the release path and the registry gate earn their weight. ISR turns freshness into a per-data-type decision, and tuning revalidation windows against tag granularity is ongoing judgment: too coarse and an edit rebuilds too much, too fine and the tag scheme becomes its own thing to maintain. The pooled ODBC bridge deliberately bounds concurrency to protect the legacy database, so a burst of regenerations queues rather than fanning out. And the migration bridge is real coupling for now: one endpoint reaches into two cache systems and even shells out to clear legacy files, which is acceptable precisely because it is scoped, documented, and dated for removal, not because it is clean.

The decision that aged best is putting tenancy in the route and brand in the data. Because the registry drives routing, prerendering, and theming from one source of truth, adding a brand never became an engineering project, and running two generations of the fleet at once stayed a content-and-cache problem instead of a fork. The lasting lesson is narrow and reusable: when sites differ only by brand, make the brand data, and a migration that would have been a fleet of rewrites becomes storefronts moving across a seam one at a time.

Next deep dive

Teaching an API to Read the Catalog

How a central Express platform separates every route across four authentication tiers, and how an OCR pipeline turned catalog page assignment from weeks of manual data entry into a reviewed batch update.

About a 6 minute read