From Monorepo to Polirepo
How we broke a bloated Turborepo monorepo into 7 independent, fast, focused repositories — and cut CI times by 72% in the process.
A monorepo feels right — until it doesn't. For the first six months of Nexum's life, a single Turborepo repository held everything: the Hono API backend, three SvelteKit frontends, shared packages, database schemas, and the CI pipelines that tied them all together. It was productive. It was convenient. And it was slowly suffocating us.
On July 1, 2026, we split that repository into 7 independent repos. This is the story of why, how, and what we learned — with 327 CI runs of real data to back it up.
The monorepo tax
Monorepos are great when your team is small and your codebase is coherent. Turborepo made it bearable with caching — turbo run build --filter=my-app skipped unmodified packages. But the cracks were showing:
- CI was slow and wasteful. Every push to any app triggered the full monorepo CI pipeline. A typo fix in the admin panel ran the same pipeline as a backend migration. Our Dependabot PRs each burned 4–6 minutes of full-stack CI for a single dependency bump.
- Deployments were coupled. You couldn't ship a backend fix without also deploying frontends that hadn't changed. Turborepo's filter helped locally, but CI and CD pipelines still operated on the whole graph.
- Git history was noise. A single PR could touch 6 different projects. Code review meant wading through changes in unrelated areas. Cherry-picks and release branches became exercises in patience.
- You needed the whole thing to do anything. New contributors had to clone the entire monorepo to work on a single frontend app.
bun installinstalled every dependency for every project.
Why polirepo?
We didn't decide to split lightly. Splitting repos is irreversible in practice — joining them back is harder than keeping them together. But the calculus was clear: the convenience of a monorepo was costing us more in friction than it saved in sharing.
Each app in Nexum has a different lifecycle. The backend changes daily. The client-facing UI changes weekly. The admin panel changes monthly. The shared packages change rarely. They shouldn't share a deploy cycle.
And so we went from one repository to seven:
The 7 repos
The numbers
Three weeks after the split, we pulled every CI run from GitHub Actions — 46 successful runs from the old monorepo, 281 from the split repos — and compared them. Same runners, same workflows, different architecture:
Methodology: CI run durations from the GitHub Actions API, July 2026. Monorepo: 46 successful runs. Split repos: 281 runs across 7 repositories. Averages are weighted by run count. The monorepo's worst outlier (52 min — a stuck job) is excluded from its p95.
How we did it
The split took two days. The process was mechanical:
Repository audit
Mapped every internal dependency. Which packages did each app consume? Which files were shared but had no package boundary? Found 4 shared utilities that became @nexum/* packages.
Extract nexum-shared
Created the shared packages repo. Migrated @nexum/theme, @nexum/api-client, @nexum/i18n, and @nexum/token-storage. Published to GitHub Packages with CI/CD.
Clone + filter
Used git filter-repo to extract each app's history into its own repo. Each repo kept its full git history — no lost commits, no broken blame.
Fix imports + CI
Updated all cross-repo imports to use npm package references. Wrote CI pipelines for each repo independently.
Validate + deploy
Ran the full test suite across all repos. Deployed each app from its own CI pipeline. Everything green.
Cut the cord
Archived the old monorepo. All development moved to split repos. Kept a workspace-level bootstrap.sh to keep local dev ergonomic.
The hidden win: AI token economy
This one surprised us. After the split, we noticed that our AI coding agents started costing less — and producing better results with fewer iterations. The reason is simple: context.
A monorepo with 6 apps, a dozen packages, database schemas, and CI configs is a massive context window. Every time you ask an AI agent to write or debug code, it needs to ingest the relevant part of the codebase. But in a monorepo, "relevant" is hard to define — the embedding or retrieval step pulls in unrelated modules from other apps, other packages, other concerns. You burn tokens on noise.
After the split, each repo has a focused surface area. A SvelteKit frontend repo contains only frontend code. The backend repo contains only API code. When an AI agent works on nexum-backend, its context window is filled with backend code — not admin panel routes, not client UI components, not shared package type definitions that aren't relevant.
The result: fewer input tokens per task, fewer wasted tokens on irrelevant context, fewer retries because the model wasn't confused by unrelated code. I checked opencode's token database to quantify this — but it was created after the split, so there's no pre-split baseline to compare against. What I can show instead is the codebase surface area: the monorepo had 831 source files across 6+ apps. The largest split repo (nexum-ui-business) has 282. The smallest (identitas-ui) has 20. An AI agent's retrieval index is ~3–5× smaller per task now, and every token saved on irrelevant context is a token available for the task you actually care about.
What we lost
I'm not going to pretend the split was all upside. Here's what we miss:
- Cross-repo refactors are slower. A rename that touches the backend and the client now requires two PRs, two reviews, and coordinated merge timing. Turborepo made this a single commit.
- Local dev needs a bootstrap script. You can't just
git clone && bun install && bun run devanymore. Ourbootstrap.shhandles cloning all repos and setting up the workspace, but it's one more thing to document. - Shared type definitions need explicit versioning. In the monorepo, you imported from
@nexum/typesand it was always in sync. Now packages have versions, and mismatches can happen.
These are real costs. But they're mechanical costs — you can script around them. The cost of a bloated monorepo was structural — you can't script around slow CI.
Polirepo was the right call
Three weeks in, I'd make the same decision again. The split gave us:
- Independent deployments. Ship the backend without touching anything else.
- Faster CI. Average 2.0 minutes instead of 7.1.
- Cleaner git history. Each repo tells one story.
- Smaller clones. Clone only what you need to work on.
- Clearer ownership. Each repo has a single purpose.
The monorepo served us well for the first 6 months of Nexum. When we outgrew it, we split. That's the right lifecycle: start together, split when the friction exceeds the integration benefit. Knowing when is the hard part — and waiting until your CI averages 7 minutes is probably waiting too long.
Methodology note: every number in this post comes from the GitHub Actions API — 46 successful CI runs from the old monorepo, 281 from the seven split repositories, pulled on July 27, 2026. No estimates, no cherry-picked best cases. The code that gathered this data runs against the public GitHub API; anyone with access to our org can verify it.