August 14, 2026·8 min read

One engine, many domains

How I stopped building websites and started building the machine that builds them — the multi-tenant pattern behind a sports network, a 50-state platform, and a family of atlases.

By Andrew Pyle

The first time I built a good site, it was a project. The second time I wanted a nearly identical site for a different subject, I almost rebuilt the whole thing from scratch — new repo, new models, new deploy — before I stopped and asked the only question that matters: what is actually different between these two? The answer was almost nothing. The data was different. The colors were different. Everything underneath was the same. That gap — between what changes and what I was about to rewrite — is where the real work lives.

This is a build-log about the pattern I use across several of my networks: one backend and one frontend serving many public sites, where standing up the next site is a configuration change, not a codebase. It sounds like an abstraction. It's the most practical decision I've made.

01The wrong unit

The wrong unit of work

A network is not N websites.

The trap is counting. "I want fifty state sites" sounds like fifty projects, and if you build it that way it is fifty projects — fifty deploys, fifty places a bug can hide, fifty times you fix the same thing. Every instinct in web development pushes you toward one-repo-per-site, because that's how the tutorials are shaped.

A per-state site isn't fifty projects. It's one platform that knows what state it's in.

Once you say it that way, the architecture writes itself. There is one thing — the platform — and it takes a parameter. The parameter says which tenant am I right now, and everything else is derived from that. USA Prep Football is exactly this: a national hub plus a per-state site, all templated for every state in the country, all one platform. A subset of the fifty are live today; the rest are the same code waiting for a config row.

Counting is seductive because volume looks like progress. But fifty near-identical repos is fifty times the work still to do, forever — one CSS fix, fifty pull requests. The number I want to make large isn't the count of sites; it's the fraction that share a single engine.

02What differs

What actually differs per tenant

Data, theme, copy. That's the list.

When you force yourself to enumerate the real differences between two sites in a network, the list is short and never grows the way you fear. For The Sports Machine — a hub plus a dedicated site for each sport across pro and college football, basketball, baseball, and hockey — a new sport is not new code. It's a theme and a data binding.

# a tenant is a small, boring object — not a codebase
TENANT = {
  "slug":    "the-baseball-machine",
  "sport":   "baseball",
  "scope":   "pro+college",
  # theming is data, not a rewrite — OKLCH via CSS custom props
  "theme": {
    "--accent":      "oklch(0.68 0.17 145)",
    "--accent-soft": "oklch(0.68 0.17 145 / 0.12)",
  },
}

Because color lives in CSS custom properties, "make the baseball site green and the hockey site blue" is one object, not a stylesheet fork. The layout, the standings schema, the structured data, the cross-linking — all shared. The engine reads TENANT at the top of the request and everything downstream asks it questions.

The honest test of the list is the thing you want to special-case. When I reach for "just this once, the hockey site needs a different navbar," that's the signal to stop — either it becomes a named field on the tenant object that every tenant could set, or it doesn't exist. A per-tenant override that isn't first-class config is a fork wearing a disguise.

Building one great sports site is a project. Building a system where a new sport is a config change, not a rewrite — that's the actual work.

03The extraction

The extraction is the product

The Atlas Network was a refactor that became a platform.

The clearest version of this happened with maps. I built BBQ Atlas first — a map-first store of entities with a renderer, entity pages, and the SEO scaffolding to make every joint a real crawlable URL. Then I wanted a completely different atlas about military history, and hit the same fork: rebuild the map-and-entity stack, or extract it.

I extracted it. The Atlas Network is that extracted engine — the data models, the map renderer, the entity-page pipeline, the SEO scaffolding — made multi-tenant so that a new atlas is a configuration and a dataset, not a new application.

BBQ Atlas

Barbecue joints, dishes, and pitmasters — geography as the primary lens. The first tenant, and the one that defined the engine's shape.

The War Atlas

Wars, battles, commanders, factions — source-cited to open data. Completely different subject and data, running on the exact same engine underneath.

Two atlases, one machine. The subjects share nothing; the platform is identical. That's the tell that the extraction was real — when the second thing you point it at has no business resembling the first, and it still just works. The first tenant is always secretly a single-purpose app pretending to be a platform. The second forces you to separate what's genuinely general from what was just BBQ-shaped assumption.

04The tenant seam

The seam that must never leak

The scariest line in a multi-tenant codebase is a query that forgot which tenant it was in.

When every site is really one platform taking a parameter, the illusion holds only as long as every read and write stays scoped to the tenant on the request. One unscoped query and the baseball site renders a hockey standings table — or a per-state site surfaces another state's data on a page that should never see it. This bug is invisible in single-tenant code, because there is only ever one right answer to "whose data is this?" It appears the instant there are many.

So the scoping can't be a thing I remember to add — it has to be structural. The tenant is resolved once, at the top of the request, and everything downstream inherits it by construction. Migrations get the same care: every migration has to run cleanly against every tenant at once, because there is one database behind all of them, and a change that's harmless for one tenant's data shape can be a landmine for another's. The safety lives in the seam, not in my attention.

05One fix, everywhere

One fix, every site

The upside of the shared seam is the mirror image of its risk.

Because there is one engine, an improvement I make once lands on every tenant the day I ship it. When I tighten the structured data on the entity pages, every atlas and every sport gets richer search results in the same deploy — they all emit the same JSON-LD contract, so I improve the contract, not fifty copies of it. The shared blast radius that makes a bad change dangerous is exactly what makes a good change land everywhere for free.

The unglamorous SEO plumbing works the same way. Sitemaps, canonical tags, the cross-linking between a hub and its leaves — none of it is hand-maintained per site. It's generated by the platform from the tenant's own data, so a per-state site's sitemap is never something I edit; it's a function of what that tenant contains. A single operator can't hand-tune search plumbing across dozens of sites, and doesn't have to: the tuning happens once, in the engine.

06The trade

What you buy, and what it costs

The platform is the asset. Each site is an expression of it.

The economics are the whole argument. When the platform is the durable thing, a fix propagates to every tenant at once, and spinning up the next vertical is cheap — because the pattern is already built. The marginal site approaches free. That's what lets a single operator run a network instead of a website.

It isn't free, though, and I'd be lying if I skipped the cost. A shared engine means shared blast radius: a bad deploy is a bad deploy everywhere, so the discipline around migrations, feature flags, and per-tenant overrides has to be real, not aspirational. You trade fifty small, isolated risks for one large, shared one, and you pay for that trade with rigor.

There's a subtler cost too: a cheap next site is a cheap next mistake. When a tenant costs almost nothing to stand up, the temptation is to launch a lot of them on thin data — and templated pages built on nothing are how a network becomes a liability instead of an asset. The engine that makes the marginal site free doesn't make it good; that stays a human decision.

The networks that stay behind a CDN help here: the per-state USA Prep Football sites sit behind Cloudflare, hub and states cross-linking each other, so the edge absorbs traffic and the origin stays one small thing.

The mental shift that makes this work is refusing to count sites. The moment I hear myself say "I need ten of these," I've already made the mistake — I should be saying "I need one of these that takes a parameter." A network isn't an achievement of volume. It's an achievement of sameness: the discipline to find the one engine hiding inside what look like ten separate ambitions. Build the machine that builds the sites. The sites are the easy part.