September 15, 2026

Moving Taproot from multiple repositories into a monorepo made our work faster and better. We had fewer releases to coordinate, spent less time waiting on CI/CD, and could make related changes together. The AI assistant could see the whole solution, and the changes it proposed were easier to integrate.

It also got us out of a familiar chicken-and-egg problem. One piece needed an update before another could use it, but finishing that update meant working through both pieces together.

I don't have a controlled before-and-after measurement that puts a percentage on the improvement. What I do have is the experience of working both ways, plus a Git history that shows the changes we could make together and the release steps we eventually removed.

If you're a small team building one connected product with AI assistance, I think those details will help you more than a blanket recommendation to put everything in one repository.

Cartoon gray cat in a green sweater running while carrying a huge orange folder with an upload arrow

One repository, several services

A monorepo keeps several projects in one version-controlled repository. A polyrepo, or multi-repo setup, gives each of those projects its own repository.

Taproot is a publishing platform. Its authoring interface, backend API, site generator, and image processor each have their own job. The frontend and Node services are written in TypeScript, and the backend is C#. Shared protobuf definitions describe the service contracts, which is the basis of our contract-driven development with generated clients.

In December 2025 our main repository brought the API, frontend, generator, and contracts together. We kept consolidating related code after that. Today I can look at any of those parts alongside its tests, development configuration, and task history.

The services still have their own jobs and build processes. Putting them in one repository didn't turn them into one running application. It gave us one place to make and review changes that touch several of them.

We also don't use a monorepo build tool like Nx or Turborepo, or pnpm workspaces. The repository is organized by folders, each service keeps its own project files and scripts, and npm workspaces only show up inside the frontend's own directory.

So when you're weighing a monorepo, keep in mind that where the source lives and how you deploy it are two separate decisions.

A package boundary that was costing us time

A later consolidation shows what improved.

I had put Taproot's generator-owned web components in a separate repository because I thought keeping them apart would make things easier. Looking back, that choice added work.

We published those components as a package. The site generator used it, and so did the previews in the authoring interface. That meant a component change could affect both what a creator saw while editing and what showed up on the published site.

Only one codebase consumed the package: Taproot. It was also tightly tied to Taproot's templates, design system, and published assets. The independence I expected when I split it out never really showed up. I've written about the cost of depending on packages you don't control, and this was an internal package, but it still made us pay for releases.

Our migration notes recorded 23 published versions and more than a dozen dependency-version updates in the generator. We had publishing workflows and a cross-repository release trigger just to keep the pieces connected.

We moved the component source into the main repository and pointed the generator and the editor preview at it there.

Part of the workBefore this consolidationAfterward
Make a component change available to its consumersPublish the package and update dependency versionsChange the source and affected consumers in the same checkout
Review the implementation and integrationFollow changes across repositories and package versionsInspect the related changes together
Coordinate the component releaseMaintain a separate package publication and notification pathInclude the component source in the product build

We kept versioning for the runtime that gets delivered to published websites. That version identifies an actual deployed artifact, so it still earns its keep.

What we removed was an internal publishing step for code we were developing as part of the same product.

What became easier for the AI assistant

A feature often crosses the lines you'd draw on a repository diagram.

An early example in our Git history is the January 2026 work on setting a site's favicon, the small icon associated with a website. One commit updated the settings contract, the backend logic and data structures, the generated TypeScript client, and the interface that selected the image and retrieved its resulting URL.

That was one product capability spread across several layers. We changed the contract and its consumers together in a single commit, and the frontend work was built against the matching backend work in the same checkout, before either was released on its own.

That kind of coordination is exactly what used to cause our chicken-and-egg problem. With the work in one repository, we could prepare the combined change first and then put it through the release process.

In our monorepo, the assistant can follow that path through the actual code. It can read the contract, find its consumers, look at an existing example, and propose the related changes together. Then I review whether the pieces agree as part of the same change.

That makes it practical for me to describe the behavior I want and let the agent work out the implementation across layers. A request still needs a clear scope and acceptance criteria. The checkout gives the agent the code to check those requirements against.

As the assistant got that broader view, I saw the quality of its proposed solutions go up. It could see how the parts actually connected, so its changes fit in more easily.

AI agents can also work across multiple repositories when the tools and permissions give them access. That would solve part of our problem, but separate commits, dependency versions, and releases would still need coordinating. For Taproot, bringing related work together made the process simpler for me as well as for the assistant.

We keep the task's explanation in the repository too. I've written about why we put our backlog in Git. That way the intended behavior, the implementation, and the verification record travel together.

A larger repository still needs focused context

Just because an agent can reach the whole repository doesn't mean it should read all of it for every task.

The context that matters for a change is usually a smaller set: the requirement, the relevant contracts, implementations, consumers, tests, and architectural rules. A repository full of unrelated files can make that search harder if the agent has no guidance about where to start.

Anthropic's Claude Code best practices talk directly about the limits of the context window. They recommend pointing to specific files, giving relevant examples, and managing unrelated material in a session. A monorepo doesn't make those limits go away.

In Taproot, service directories and READMEs give the agent a place to start. Our project instructions describe the architectural rules and point to examples. Task records explain what needs to change and what counts as done.

The assistant still has to search, follow references, and decide what to read. Keeping related code close together makes that easier to set up, but it doesn't guarantee the agent will find every dependency.

Whether this cuts token usage depends on the task and how the agent pulls in context. We haven't measured token savings. We keep the arrangement because the workflow is simpler and the work we're producing is better.

Moving the files was only part of the migration

The component consolidation also turned up some details that needed care.

Our editor and generator each had their own dependency installations. Once the editor started importing source from the generator directory, module resolution could pick up a different physical copy of the design-system dependency. Bundling both copies would cause duplicate custom-element registration, so we configured resolution to make the editor bundle use one copy.

The Docker build needed work as well. A local checkout had the new shared source, but the editor's image build only had the files its Dockerfile copied, so we had to add the component source to that build. TypeScript, the bundler, and the test runner also needed matching import paths.

Count those migration costs when you make the decision. Getting an import to work locally doesn't prove that your tests, container builds, and release workflows all understand the new layout.

Keeping the convenience without losing the boundaries

When code sits close by, it's easier to reuse. It's also easier to pull in a dependency that shouldn't be there.

Taproot still has rules about which layers can import which others, how API contracts are defined, and where mutations and authorization belong. Automated guards enforce parts of those rules, which I cover in more detail in how we enforce coding standards for AI-written code. Just because the agent can find a file doesn't give it permission to wire that file into anything else in the repository.

Verification needs the same care. Our root verification script picks checks based on which paths changed. A change to shared contracts can trigger checks across several services. That mapping has to be maintained as the relationships change.

A monorepo also doesn't make production updates atomic. Services and published sites may run different versions during a rollout. Compatibility and release ordering still need deliberate handling, even when the source changes share a commit. The published-site runtime is one reason we keep versioned release contracts inside Taproot's monorepo.

The shared checkout makes coordinated work easier to review. Whether that work is ready to use is still up to the builds, tests, and deployment controls.

When I would keep separate repositories

For a small team, I'd start by looking at how often a useful product change crosses repository boundaries.

If the frontend, backend, and shared contracts routinely change together, a single repository deserves a serious look. The same goes for an internal package that has to be published over and over just to get an update into the only product that uses it.

Separate repositories can fit better when you need to restrict access by repository, when products share little code, or when a library has independent consumers who need a stable release contract. In those cases I'd keep the boundary.

If the main problem were finding related code, I'd also try improving a multi-repo workspace before migrating. Moving repositories has to pay back the effort of changing builds, CI, permissions, and documentation.

If you're planning a product, the practical question is how much coordination a normal feature takes. Pick a recent change. Where did it start, which components needed updating, how many releases did it take, and how did you check the combined result? The answers give you a solid basis for choosing a structure.

Why I'm keeping the monorepo

For Taproot, the monorepo fits the way we build. I describe a product change, the assistant investigates the relevant layers, and we develop and verify the result in one shared working environment.

It has also let us get rid of machinery whose main purpose was shuttling changes between closely related parts of the same product, and the work is a lot easier without it.

This is one way I'm applying 26 years of full-stack engineering experience to working with AI: looking at the process around the code and changing the parts that get in the way of shipping it.

If you're building a product and want help making that process work better, I'd be glad to talk about the software, the constraints, and where the work is getting stuck.