Skip to content

Monorepo vs polyrepo: why we run both

For CTOs and engineersCTO decisions · Updated

Filip PapranecFounder, Digitaliko

Monorepo vs polyrepo, why we run both. Illustration: a tree of separate repositories, each with its own lock, next to one repository with many packages wired together.Monorepo vs polyrepo, why we run both. Illustration: a tree of separate repositories, each with its own lock, next to one repository with many packages wired together.

Monorepo vs polyrepo is a question of which boundary you need. A monorepo puts many packages in one repository, so one change can cross all of them. A polyrepo gives each project its own repository, so each one can have its own access, CI and release.

We run both on purpose. Every product we build is a Turborepo monorepo. The company itself is a polyrepo: every client, every service and every confidential area is its own repository, tied together with git submodules. An AI agent works in both every day.

What is the difference between a monorepo and a polyrepo?

The short version: a monorepo draws a code boundary around packages, a polyrepo draws an access boundary around repositories.

MonorepoPolyrepo
One change across many partsOne commit, one pull requestOne pull request per repository
Shared codeA workspace package, imported directlyA published package or a copy
Who can read whatEveryone with access sees everythingAccess per repository
CIOne pipeline, a build tool skips what did not changeOne pipeline per repository
Dependency versionsOne lockfile, one version per packageEach repository drifts on its own
Context for an AI agentThe whole system in one checkoutOnly what the task needs, the rest by navigation

Most articles on this topic argue for one column. We think both are right, for different units. The question is what you are drawing the line around.

Why is every product a monorepo?

A product is code that ships together. Our template and wizrd, our internal Claude Code plugin and CLI (not public), look the same: one app and the packages it is made of.

{
  "packageManager": "bun@1.3.13",
  "workspaces": {
    "packages": ["apps/*", "packages/*"],
    "catalog": { "zod": "^4.1.13", "typescript": "^6", "better-auth": "1.5.5" }
  },
  "scripts": { "build": "turbo build", "check-types": "turbo check-types", "deploy": "turbo -F @wizrd/infra deploy" }
}

apps/web is the app. packages/ holds api, db, auth, infra, ui, env and a few more. The app imports them as workspace:*, so there is nothing to publish. The catalog pins shared versions once, so the API and the app never run two different versions of the same library.

Turborepo sits on top with a short turbo.json:

{
  "tasks": {
    "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] },
    "check-types": { "dependsOn": ["^check-types"] },
    "dev": { "cache": false, "persistent": true },
    "deploy": { "cache": false }
  }
}

^build means: build what I depend on first. Builds, type checks and lint are cached. Deploys and database pushes never are. The cache is the reason the Turborepo docs give for using it at all:

Turborepo uses caching to speed up builds, ensuring you never do the same work twice.

Turborepo docs
Caching, as of 2026-10-24

The real win is not speed, though. It is the single pull request. When the database schema changes, the API, the types and the screen change in the same commit, and check-types fails if one of them was missed. For an AI agent that is gold: the type checker tells it what it broke before a person has to.

A client's online marketplace runs the same shape, with its crawler as one more app in the monorepo.

Why is the company a polyrepo?

Because access is the boundary that matters most to us, and GitHub grants access per repository. There is no read permission for a single folder. So everything that needs a different audience gets its own repository:

  • Client repositories for each client, readable by the team that works for that client. A freight forwarder we built and run a system for has its own client repository with its service repositories inside it as submodules.
  • Rooms for confidential areas such as finance and brand, each readable only by its own GitHub team.
  • Service repositories for our own tools, such as the plugin and harness our agents use.

The company repository at the root ties them together with git submodules. Nobody gets the whole tree by default. A session without access to finance cannot read finance, whatever its prompt says.

Monorepo vs polyrepo side by side. Left, the company as a polyrepo: a root repository with client repositories, rooms for finance and brand and service repositories as git submodules, each with its own GitHub team. Right, one product as a Turborepo monorepo: apps/web and packages for api, db, auth, infra and ui, built in dependency order with a shared cache.Monorepo vs polyrepo side by side. Left, the company as a polyrepo: a root repository with client repositories, rooms for finance and brand and service repositories as git submodules, each with its own GitHub team. Right, one product as a Turborepo monorepo: apps/web and packages for api, db, auth, infra and ui, built in dependency order with a shared cache.

This is the part a monorepo cannot give you. A monorepo with a "please do not open the finance folder" rule is a rule. A separate repository with its own team is a lock.

How does an AI agent work across both?

The agent follows the same few rules on a person's laptop and on its own machine:

  1. Navigate, never clone

    Every repository is a submodule in the tree. The agent goes to clients/<x>/ or services/<y>/ instead of cloning a second copy somewhere else.

  2. One worktree per task

    Each task gets its own git worktree. A client service is nested inside a worktree of its client repository, so the session loads every CLAUDE.md up the chain.

  3. Work in one repository

    Commits and pull requests go to the repository the change belongs to: client work to the client, service work to the service.

  4. Pointers move separately

    A task branch never moves a submodule pointer. A separate sync step does that, after the work is merged.

The last rule is enforced by a script, not by trust. Our gate fails a task branch that changes a gitlink, the git object behind a submodule pointer:

moved=$(git diff --raw "$base"...HEAD | awk '$1 ~ /160000/ || $2 == "160000" {print $NF}')
[ -z "$moved" ] || { echo "FAIL submodule pointer changed: pointers move only via sync"; exit 1; }

Without it, an agent that touched two repositories in one task would quietly move the parent's pointer to an unmerged commit. Everyone else would then check out code that only exists on a branch.

The nested worktree is the other detail that took us a while. A plain worktree of a service would miss the client's CLAUDE.md one folder up, so the agent would work without the client's rules. Nesting the service inside a worktree of its client keeps the whole chain of context.

What does a polyrepo cost?

It is not free, and we would not pretend otherwise:

  • Changes across repositories take several pull requests. A rule that changes for every client means a pull request per client repository.
  • Submodules have rough edges. A fresh checkout needs git submodule update --init, and a stale pointer shows old code without any warning.
  • CI is configured many times. Each repository needs its own workflows.

We solved the last one by not copying CI. Every repository calls thin shared actions from one tool repository, pinned to a moving major tag. A new validation check lands in one place and reaches every repository on its next run. They all run on one self-hosted GitHub runner for the whole org.

Monorepo or polyrepo: how to decide for your team?

Go through what is true for your code today:

Turborepo vs Nx comes up next. Honest answer: we use Turborepo because our monorepos are TypeScript apps on Bun workspaces and a short turbo.json is all they need. We have not run Nx in production, so we will not rank them.

How we run several agents in parallel with worktrees is in Claude Code subagents and worktrees. The whole loop from issue to pull request is in Harness engineering in practice.

Need repositories an agent can work in safely?

We set up the repository layout, the shared CI and the agent rules right in a team's codebase as part of AI-native SDLC for development teams. Is your team short of hands rather than structure? See a senior developer for your team. AI writes all of our code today, and a senior is accountable for the result. Since 2023 we have delivered 15+ production projects.

We get back to you within two business days, and together we pick what to fix first.

If you'd rather talk first, book a 15-minute meeting with Filip.

Where does your code need a code boundary, and where an access boundary?

In short

  • A monorepo draws a code boundary, a polyrepo draws an access boundary.
  • Each of our products is a Turborepo monorepo: one pull request changes schema, API and screen together.
  • The company is a polyrepo of submodules, because GitHub grants access per repository.
  • The agent navigates instead of cloning, works in a nested worktree, and a gate blocks submodule pointer moves.

Frequently asked questions

What is the difference between a monorepo and a polyrepo?

A monorepo keeps many projects or packages in one git repository with one history. A polyrepo gives each project its own repository. In a monorepo one commit can change several packages at once. In a polyrepo each repository has its own access, its own CI and its own release.

When is a monorepo the better choice?

When the parts ship together and share code: a web app, its API, its database schema and its UI components. One pull request changes all of them, and the types catch what no longer fits before anything is deployed.

When is a polyrepo the better choice?

When different people may see different parts. GitHub grants access per repository, not per folder, so client code, finance and internal tools that need different audiences belong in different repositories.

Turborepo or Nx?

We use Turborepo with Bun workspaces, because our monorepos are TypeScript apps and Turborepo only needs a small turbo.json on top of the workspaces. We have not run Nx in production, so we cannot compare them from experience.

Can an AI agent work in a polyrepo with git submodules?

Yes, if it has rules. Ours navigates into the submodule instead of cloning a copy, works in its own worktree per task, and a gate blocks any task branch that moves a submodule pointer.

Ask AI about this article

The AI also draws on our other articles and experience from real projects.

ChatGPTClaudePerplexity
Engineering

Other articles

CTO decisions

Claude Code pricing for a team: plans, API and agents

Claude Code pricing from Anthropic's public list (October 2026): Pro, Max, Team seats, Enterprise and API tokens. Which one fits developers, which one an always-on agent and a CI reviewer run on, and the guardrails that stop a background agent from burning the budget.

CTO decisions

Vercel alternatives: why we run Vercel and Cloudflare

We did not replace Vercel, we split the work. Next.js sites stay on Vercel, new apps run on Cloudflare Workers and D1, declared in TypeScript with Alchemy. Pricing with sources, our config, and the deploy block an AI agent hit on Vercel.

CTO decisions

Better Auth vs Clerk: why we chose Better Auth

Better Auth vs Clerk comes down to where your users and sessions live. We moved from NextAuth to Better Auth in every new app and never picked Clerk. Our real config, the plugins we run, Clerk pricing and when Clerk is the better call.

Time to stop planning and start solving.

Contact

  • Digitaliko s.r.o.
    Šípková 377/17
    958 03 Malé Uherce, Slovakia
  • Get in touch
    info@digitaliko.com
    +421 950 595 506
    WhatsApp