Manage Multiple Websites From One Codebase

  • Aug 20

How to Manage Multiple Websites From One Codebase (And the One Thing I Refuse to Share)

Ten live sites run from one repository. What is shared, what stays per-site, the deploy and DNS traps, and when to split a site out.

I run ten live websites out of one repository. Ten domains, ten brands, ten different things being sold, one folder on my machine. When I push, up to ten deploys are triggered and most of them get skipped on purpose.

Most of what gets written about running many sites from one codebase stops at the architecture diagram. You get a definition, a comparison against separate repositories, a paragraph about sharing components, and then it ends, right before the part that actually takes your weekend: the deploy topology, the DNS, the environment variables that are different for every single site, and the moment a shared component becomes the thing holding two brands hostage.

This is the other half. What my network looks like after ten launches, what I actually share between the sites, what I refuse to share and why, and the specific things that broke.

What "one codebase, many websites" really means once the sites are live

There is a word game buried in this topic that costs people a lot of time.

Most multisite tooling sells you one codebase serving many sites at runtime. One deployed application, one process, and a lookup on the incoming hostname that decides which theme and which content to render. That is the right answer when your sites are variations of each other: a company site and its careers microsite, twelve university departments, forty franchise locations with the same layout and a different address on each one.

My ten sites are not variations of each other. One is a typing test that sells verified certificates to job applicants. One is an invoicing tool for freelance developers. One is a paid directory of legal AI software. They share a founder and nothing else. A runtime hostname lookup would be exactly the wrong shape.

So I run the other model: one repository, ten independent applications, ten separate builds, ten separate deployments, ten separate domains. Shared at the source level, never at runtime. Nothing about site four is in memory when somebody visits site nine.

That single distinction decides everything downstream, so pick it before you write any code. If your sites are one product wearing different hats, use runtime multisite. If your sites are different products that happen to have the same owner, use one repository and many builds.

What I actually share across ten sites

One dependency tree and one install

The repository is a workspace. The root file lists a single glob for the apps folder, and every site lives in its own directory under it. One install at the root, one lockfile, one copy of the framework on disk instead of ten. The npm workspaces documentation covers the mechanics, and the useful part in practice is that you get one command that builds every site and one that typechecks every site.

Each site keeps its own package file with its own build, start and typecheck scripts, and its own development port so I can run four of them locally at once without a collision.

There is a hard rule in my build standards that looks fussy and is not: nothing may run an install, add a dependency, or edit any package file. Dependencies are pre-installed at the root. If something is genuinely missing, the run reports it instead of installing it. That rule exists because an agent adding one package to one site quietly rewrites the lockfile for all ten, and the next deploy of a completely unrelated site rebuilds against a dependency tree nobody reviewed.

A standards document instead of a shared component library

This is where I part ways with the usual recommendation.

The standard advice is to extract everything repeated into shared internal packages. A shared UI package, a shared config package, shared utilities. I do not have one. There is no packages folder anywhere in this network. Ten sites, zero shared components.

That is deliberate, and it is the decision I would defend hardest.

A shared component library forces visual convergence. Give ten brands the same button, the same card and the same header component and you get ten sites that unmistakably came out of one template. Anyone comparing two of them can see it in about four seconds. These are supposed to be ten separate businesses that a buyer could believe in individually, so each one gets its own look, hand-rolled, with its own colour system and its own typography.

What I share instead is a document. The build standards file specifies that every app is Next 15 with the App Router, TypeScript in strict mode, Tailwind v4, statically generated with a short explicit list of API routes, one heading-one per page, a sitemap route and a robots route, an author system with a real person in the structured data, a blog loader that reads markdown out of a content folder, hero images as WebP at fixed dimensions so nothing shifts while loading, analytics in the layout, an accessibility floor of semantic landmarks and visible focus states, and a banned-words list for all copy.

Every site implements those rules in its own code. The rules are shared. The implementation is not. When I raise the standard, I change the document, the next site built follows it, and the existing sites adopt it on their own schedule instead of all ten moving at once behind a version bump.

The cost is honest, so here it is: I have written the same blog loader ten times. It is roughly forty lines. Ten copies of forty lines is a rounding error next to the alternative, where a shared package with ten consumers turns every small change into a ten-site regression test, and where the one site that needs the loader to behave differently gets a config flag bolted on instead of four lines of its own.

A manifest that says what every site is

One file at the root lists every site: folder name, domain, what type of business it is, its niche, what it monetizes, whether it uses Redis, whether payments are wired, whether it is eligible for the daily blog cadence, whether it has launched.

Nothing in the applications reads that file at runtime. It exists entirely for the automation. The publishing routine reads it to decide which sites get a post today, the launch routine reads it to know what still needs finishing, and I read it when I have forgotten which of these things sells what. The business case for structuring a portfolio this way is a separate argument, which I made in how I monetize a domain portfolio from one repo.

What has to stay per-site, with no exceptions

The three places a new site must be registered

A new app is not real until it appears in three separate places: the root workspace list, the network manifest, and the local preview configuration with its own development port.

Miss the workspace list and the build-everything command silently skips the site. Not an error. It just is not there, and you find out when you notice the deploy count did not change. Miss the manifest and the site launches fine and then never receives a blog post, because the routine that writes posts does not know it exists. Miss the preview config and you cannot run it locally.

None of those three failures announces itself. All three look like everything worked.

One deploy project per site, pointed at a subdirectory

Each site is its own project on the host, with the root directory set to that site's folder inside the repository. Ten imports, ten projects, one repository, as described in Vercel's monorepo documentation.

Three things about that are worth knowing before you start rather than after.

By default, a push creates a deployment for every connected project. Vercel will automatically skip projects whose files did not change, but only if the repository uses a real workspace definition, every package has a unique name, and the repository is connected to GitHub. Those skipped builds do not occupy a concurrent build slot, which is a small detail at ten sites and a large one at twenty.

There is also a hard ceiling. Vercel's published account limits allow twenty five projects connected to one Git repository on the Hobby plan and one hundred fifty on Pro. One repository with ten sites is comfortable. One repository with three hundred sites is not a plan.

And the import screen has a trap. The root directory picker is a modal list, and clicking it by position selects the wrong row often enough that I now match the row by its text and read the checked state back before confirming. Choosing the wrong folder here produces a project that builds a completely different site under your new domain, which is a confusing five minutes.

Environment variables belong to the project, not to the repository

One repository, ten separate environment stores. Nothing is inherited, nothing is shared, and every value has to be entered ten times: the notification address, the canonical site URL, the payment links, the storage credentials. Then again for the preview environment, because a variable set only in production means every preview build of that site runs in a half-configured state and you debug the wrong thing.

The single worst hour I spent on this network came from a name collision. Connecting the managed Redis integration injects its own variable names into the project, and those names did not match the ones the code was written against. Nothing threw. The client simply never initialized, so rate limiting passed everyone through and license lookups quietly returned nothing at all. The fix is trivial once you see it, and the code now accepts either name. Finding it was not. That whole category of problem, the configuration that lives outside your repository and therefore outside your review, is what I wrote about in the six things that do not travel with your code.

If you are running more than one or two of these, this is also roughly where the Accelerator earns its keep, because most of what I hand over there is the checklist and the working app, not the theory.

The domain traps that cost me the most time

The parked redirect that quietly beats your new address record

Every domain in this network was parked before it became a site, which means the registrar was holding a redirect record on the apex pointing at a sales page.

Add the new address record and leave the old redirect in place and the redirect wins. The site builds, the deployment is green, the host shows the domain as valid, and visitors get the old parking page. Delete the parked record first, then add the address record. I have made this mistake twice and both times went looking in the wrong system for it.

Apex versus www is a canonical decision, not a preference

When you add a domain, the host offers to redirect the apex to the www hostname, and that option is checked by default.

My entire network uses the bare apex as its canonical URL. Leave that box checked and every canonical tag, every sitemap entry and every structured-data URL on the site points at a hostname that immediately bounces to a different one. Nothing looks broken. You just spend a month wondering why indexing is sluggish. Decide which hostname is canonical once, apply it in the metadata base, the sitemap and the host settings, and never mix them.

Registrar forms lie about what they saved

Two related habits, learned the hard way. Setting a value in a registrar's DNS form programmatically can leave the form's own state thinking the field is still empty, so it refuses to save with a complaint about a missing value. And the save action is usually per row, so an unsaved row is discarded on reload without a warning. Type the value, save the row, reload the page, and read the record back. Verify, do not assume.

What keeps ten sites alive after launch week

The honest answer is that maintenance does not scale and routines do.

Each site gets one post per weekday, written and published by a scheduled routine that reads the manifest, picks the site whose turn it is, and commits to the repository. Indexing requests go out on a weekly sweep across every property. Nothing about that is me remembering to do it. If you want the mechanics of repositories that update themselves on a schedule, I covered that in how I run repos that update themselves.

When one codebase stops being the right answer

I would split a site out of this repository for any of four reasons.

It needs a different framework or a different release cadence than the rest. It needs runtime infrastructure the others do not have, like a persistent server or a queue, so the deploy shape stops matching. Someone else starts working on it and needs their own permissions, their own review rules and their own history. Or it simply gets big, and its build time starts taxing every other site in the workspace.

None of those are true for me at ten sites. All of them would be true if one of these grew into an actual company, and that is a good problem to be handed.

The order I would set this up in if I started over

  1. Decide runtime multisite versus many builds, based on whether your sites are one product or several.

  2. Create the workspace with one app in it, and get that single site deployed and live on its real domain before adding a second.

  3. Write the standards document before the second site, not after the fourth. Every rule you skip here becomes ten inconsistencies later.

  4. Add the manifest as soon as you have two sites, listing every site and what it sells.

  5. Register each new app in all three places in one pass: workspace list, manifest, preview config.

  6. Import each site as its own project with its own root directory, and confirm the first deployment goes green before touching DNS.

  7. Delete the parked redirect at the registrar, then point the apex, then verify the record you saved actually saved.

  8. Enter environment variables for both production and preview, and check that any integration you connected did not rename them.

  9. Only then wire payments, analytics and Search Console, and get the first post published.

Get the whole factory this Saturday

I am walking through this entire setup live on Saturday, August 22 at 10:00 AM Eastern: the monorepo itself, the three business skeletons the sites are built from, the manifest, the daily publishing and indexing routines, and the full launch checklist that turns a passing build into a live, indexed, paid site. You can register for Domain Factory here.

If you would rather have the whole library than a single session, the Reinventing AI Accelerator includes the live sessions plus the vault of apps, tools and agents that come out of them. One small ask either way: if you take one thing from this article, take the standards document idea. It is the cheapest thing here and it is the reason ten sites have not drifted.

Frequently asked questions

Should I use one repository or separate repositories for multiple websites?

If the sites share an owner, a toolchain and a release rhythm, use one repository. You get a single install, one set of standards and one place to run automation across all of them. Split a site out when it needs a different framework, its own permissions, its own release cadence, or when its build time starts slowing everyone else's deploys.

Can one repository deploy to multiple domains?

Yes, and this is the normal setup rather than a workaround. You create one hosting project per site and set each project's root directory to that site's folder. Each project then gets its own domains, its own environment variables and its own deployment history, all reading from the same repository. Check your plan's limit on projects connected to a single repository before you scale past a couple of dozen.

Do all the sites have to use the same framework?

No, but keeping them the same is most of the value. Shared dependencies only help when the sites want the same dependencies, and a standards document only helps when every site can actually follow it. Mixing frameworks in one workspace is possible, and it costs you the install savings, the automatic skipping of unchanged projects, and the ability to write one routine that works on every site.

How do I stop one site's build from rebuilding all of them?

On a properly declared workspace connected to GitHub, Vercel already skips projects whose files have not changed, and those skipped builds do not consume a concurrent build slot. If your repository does not meet those requirements, you can fall back to an ignored build step, which is a script that decides per project whether to proceed. The trade is that ignored builds do count against your build limits, so the automatic skipping is the better option when you qualify for it.

What happens when two sites genuinely need the same component?

I copy it. Two copies of a component are two files that can each change independently, and the day one brand needs different behaviour, that change costs four lines instead of a config flag on a shared package with ten consumers. I would only extract something into a shared package once four or more sites needed it, it had stopped changing, and it carried no visual opinion at all. Across ten sites, that threshold has never been met.

Subscribe Now for More AI Insights

Subscribe for Updates from Reinventing AI

Stay current on the most cutting-edge AI solutions for ambitious entrepreneurs and marketers!

Get weekly AI training announcements, AI resources and insights.

0 comments

Joinor login to leave a comment