Guides · July 30, 2023
Monorepo vs Polyrepo for Commerce Projects
For most Next.js + headless commerce builds — a storefront, a backend, and a handful of shared packages — a monorepo wins on velocity and type safety. Polyrepo earns its keep once teams, deploy cadences, or compliance boundaries genuinely diverge.
By Polo Themes
For the large majority of commerce projects built on Next.js and a headless commerce backend, a monorepo is the right default: one repository holding the storefront, the backend or backend-config layer, and shared packages (UI kit, types, SDK client), all versioned together and type-checked across boundaries in a single CI run. Polyrepo — separate repositories per service — earns its keep only once you have genuinely independent teams with different release cadences, hard compliance or access-control boundaries between codebases, or services in different languages that don't benefit from a shared toolchain. This guide works through the real tradeoffs for a commerce stack specifically, not generic monorepo theory.
The monorepo-vs-polyrepo debate gets litigated constantly in general software engineering circles, and most of that debate is not that useful for commerce teams, because commerce projects have a specific shape: a storefront that renders product and checkout pages, a commerce backend (headless or platform-hosted) that owns catalog and order state, and usually a scattering of shared code — a design system, a typed API client, utility packages — that both need. That shape changes the calculus compared to, say, a company with forty unrelated microservices owned by forty different teams.
What "Monorepo" and "Polyrepo" Actually Mean Here
It's worth being precise, because the terms get used loosely. A monorepo in the commerce context usually means: one git repository, one workspace tool (pnpm/npm/yarn workspaces, or a build system like Turborepo or Nx layered on top), and multiple packages inside it — apps/storefront, apps/backend, packages/ui, packages/types, and so on — that can import from each other directly, with a single CI pipeline and often a single deploy pipeline that fans out per-app. A polyrepo setup means the storefront lives in one repository, the backend in another, and any shared code either gets duplicated, published as a versioned package to a private registry, or lives in a third "shared" repo that both consume as a dependency.
The important distinction isn't really "how many git repos" — it's how tightly coupled the release process and the type contracts are. You can run a polyrepo setup that behaves like a monorepo (shared types published on every commit, synchronized releases) and you can run a "monorepo" that behaves like a polyrepo (apps that never import from each other, deployed on totally separate schedules). The repo layout is a proxy for the coupling decision, not the decision itself.
The Case for a Monorepo in Commerce Projects
Type safety across the storefront-backend boundary
This is the strongest argument, and it's specific to headless commerce. Your storefront's product page, cart, and checkout flow all depend on shapes defined by the backend — product variants, price sets, cart line items, order status. When the storefront and backend live in the same repository, you can share a single types package (or generate types from the backend's schema directly into a package the storefront imports) and get a compile error the moment a field is renamed or removed, before it ever reaches a staging environment. In a polyrepo setup, that same change either requires a manual version bump and changelog discipline across two repos, or it silently breaks the storefront at runtime — usually discovered by a customer hitting a broken checkout page rather than by CI.
Atomic changes across app boundaries
Commerce features routinely span both sides: adding a new discount type touches backend logic and a storefront cart UI; adding a new fulfillment status touches an admin view and an order-tracking page. In a monorepo, that's one pull request, one review, one CI run, and one deploy that can be sequenced correctly. In a polyrepo, it's two pull requests in two repos that have to be coordinated by a human remembering the right order to merge and deploy them in — a common source of half-shipped features and awkward feature-flagging just to avoid a broken intermediate state.
One dependency graph, one set of versions
A monorepo with a single lockfile means the storefront and backend (and any shared packages) resolve to one version of React, one version of the commerce SDK, one version of your internal UI library. That sounds like a small thing until a security patch or a breaking upstream release lands — in a monorepo you update it once; in a polyrepo you're tracking whether repo A and repo B have drifted onto incompatible versions of a shared dependency, often discovered only when a shared component behaves differently in each app.
Lower operational overhead for small-to-mid teams
Most commerce teams are not forty engineers split across independent squads — they're a handful of developers, sometimes a single founding engineer, working across the whole stack. For that team size, the coordination cost of a polyrepo (separate CI configs, separate dependency updates, separate release notes, context-switching between repos to trace a bug that crosses the storefront-backend boundary) is pure overhead with no corresponding benefit. A monorepo means one install command, one CI config to maintain, and one place to look when something breaks.
Where Polyrepo Genuinely Wins
None of the above means monorepo is always correct. Polyrepo is the better call in a specific set of situations, and it's worth naming them honestly rather than treating monorepo as a universal answer.
- Independent teams with different release cadences. If your backend team ships on a weekly, carefully reviewed schedule and your storefront team wants to deploy multiple times a day, forcing both into one CI pipeline and one set of merge gates creates friction neither team asked for. Separate repos let each team own its own release rhythm cleanly.
- Hard security or compliance boundaries. If the backend handles PCI-scoped payment logic or PII that only a subset of engineers should even be able to clone, a shared repository undermines that boundary — repo-level access control is a much simpler compliance story than path-level permissions inside one monorepo.
- Genuinely different languages or runtimes with no shared tooling benefit. If your commerce backend is, say, a Java service and your storefront is Next.js, there's no shared linter, type system, or package manager to unify — the main monorepo benefit (shared types, shared tooling) doesn't apply, and you're left with just the git-repo-count decision, which tends to favor keeping them separate.
- Open-sourcing or spinning off one piece. A component you intend to open-source, or a backend you may eventually sell or extract as a standalone product, is easier to manage with clean repo boundaries from day one rather than untangling it out of a monorepo later.
- CI runtime becomes a bottleneck at scale. A very large monorepo with dozens of apps and packages can eventually make every CI run slow if the build system isn't set up with proper caching and affected-package detection. This is solvable with the right tooling (see below) for a long time before it justifies splitting repos, but it is a real ceiling.
Head-to-Head: The Decision Factors That Actually Matter
- Type safety across storefront/backend: Monorepo wins clearly — shared types with zero publish step. Polyrepo requires either a published types package with version discipline, or codegen from an API schema, both of which add process.
- Deploy independence: Polyrepo wins if teams truly want separate cadences. A well-configured monorepo (Turborepo, Nx, or even simple per-app CI triggers keyed on changed paths) can approximate this, but it takes deliberate setup rather than coming for free.
- Onboarding and mental model: Monorepo wins for small teams — one clone, one install, the whole system visible. Polyrepo wins only once the system is large enough that no single engineer needs (or should have) the whole picture in view.
- Dependency and security patching: Monorepo wins — one lockfile, one Dependabot/Renovate config, one place to bump a vulnerable package.
- Access control granularity: Polyrepo wins where compliance requires it. Monorepos can approximate this with owner-review rules and branch protection on paths, but that's weaker than repo-level access control.
- CI speed at scale: Roughly even with the right tooling. A monorepo without build caching and affected-package filtering will feel slower than polyrepo CI; the same monorepo with Turborepo's remote cache or Nx's affected commands will often be faster, since unrelated packages are skipped entirely.
A Practical Recommendation
If you're starting a new Next.js + headless commerce project today — a storefront, a commerce backend, and shared UI or type packages — start with a monorepo. Use pnpm (or yarn/npm) workspaces for the package graph, and add Turborepo or Nx once you have more than two or three packages and start noticing redundant rebuilds; both integrate cleanly with a Next.js storefront and a Node-based commerce backend, and both support remote caching so CI doesn't rebuild unchanged packages on every run. Structure it as an apps/storefront directory, an apps/backend directory, and a packages folder for anything shared — this is the layout most Next.js + headless commerce codebases converge on regardless of which specific commerce backend they use, because it maps cleanly onto how the system actually decomposes.
Split into separate repositories only when a concrete forcing function shows up — a compliance requirement, a team that wants full release autonomy, or a piece of the system you're spinning off. Splitting preemptively, before that forcing function exists, mostly just adds coordination overhead you didn't need yet. It's also worth noting that splitting later is a well-trodden, mechanical migration (extract a directory's git history, publish its shared code as a package) — it is not a one-way door, so defaulting to monorepo doesn't lock you out of moving to polyrepo when a real reason appears.
Polo Themes builds Shopify and Figma storefront kits today — our Shopify themes and Figma UI kits cover categories like optics, medical, electronics, and course platforms. We're also building toward production-grade Next.js and headless commerce starters, applying the same repo-structure and type-safety discipline covered in this guide directly into the starter's layout, so teams adopting it inherit a sound monorepo shape rather than having to design one from scratch. That product isn't available yet, but if you're evaluating this decision today for your own build, the monorepo-first approach above holds regardless of which commerce backend you pair it with.
Frequently Asked Questions
Does a monorepo mean one deploy for everything?
No — a monorepo is about shared source and tooling, not shared deployment. Well-configured monorepo CI (Turborepo, Nx, or simple path-filtered triggers) deploys each app independently based on what actually changed, so a storefront-only change doesn't redeploy the backend.
Can I move from polyrepo to monorepo later without losing git history?
Yes. Both git's built-in subtree merging and dedicated repo-merging tools can import a repository's full commit history into a subdirectory of another repo, so this is a one-time mechanical migration rather than a rewrite.
Is a monorepo harder to secure than separate repos?
It requires more deliberate configuration — path-based code-owner rules, branch protection, and secrets scoped per app in CI — but it isn't inherently less secure. The exception is a hard compliance requirement for repo-level access isolation, where separate repos are the simpler control to reason about and audit.
What if my storefront and backend are on completely different release schedules?
That's the strongest legitimate reason to lean polyrepo, or to configure a monorepo's CI so each app's pipeline is fully independent and gated separately. Either way, decide this deliberately rather than defaulting into a shared release train just because the code lives in one repository.