← Back to blog

Ship Your SaaS MVP in 2–4 Weeks with a Monorepo for Founders

September 29, 2026
Ship Your SaaS MVP in 2–4 Weeks with a Monorepo for Founders

If your SaaS needs atomic cross-project changes and shares real code between apps, a monorepo is the right choice. Start with pnpm workspaces plus Turborepo, and only reach for Nx once you need generators or a bigger project graph. Keep every app independently deployable no matter which tool you pick.


TL;DR:

  • Use a monorepo if your changes frequently involve multiple apps, shared types, or database schemas that need to evolve together.
  • Rely on pnpm workspaces and Turborepo for minimal setup, with Nx added only for large projects requiring advanced graph analysis and code generation.
  • Implement remote caching and affected detection to prevent redundant builds and tests across machines, maintaining fast CI cycles.
  • Structure shared packages carefully around core types, auth, API clients, and schemas, avoiding generic or poorly owned utility folders.
  • Enforce strict ownership boundaries, scoped environment variables, and import rules early to maintain security and prevent dependency drift.

Hanad Kubat
Build Your First Real SaaS Version
Turn your core workflow into working software with a fixed scope, fixed price, and code you own from the first commit.
Explore the fixed-price build

Table of Contents

When to choose a monorepo and when to skip it

The decision comes down to how often your changes need to cross project lines. If a single feature touches your API contract, your web app, and a shared types package at the same time, splitting those into separate repos means coordinating pull requests across three places just to ship one thing. A monorepo removes that coordination tax because the change lands in one commit.

Shared code is the other signal. UI libraries, TypeScript types, a Prisma schema, or a generated API client all belong in one place if more than one app consumes them. Team structure matters too: if you need strict repo-level access control, where one team should never see another team's code, separate repositories give you that isolation more cleanly than a monorepo with folder permissions, a point Nx makes directly when comparing monorepo and polyrepo tradeoffs. Release cadence is the last check: apps that ship on completely unrelated schedules with no shared dependencies gain little from living together.

  • Choose a monorepo when your apps share types, UI components, or a database schema and change together often.
  • Prefer polyrepo when teams need hard access boundaries or your apps ship on unrelated cadences with no shared code.

Concrete tool mapping: pnpm, Turborepo, Nx, and remote caching explained

Three tools cover almost every SaaS monorepo setup, and each solves a different layer of the problem.

pnpm workspaces handle package linking: one lockfile, local packages linked by name instead of published and reinstalled, and catalogs that let you define a dependency version once and reuse it across every package in the workspace, which pnpm's own workspace documentation describes as the core mechanism for managing local packages and shared version ranges.

Turborepo sits on top of that and manages the task graph: it decides which build, test, or lint commands need to run and in what order, based on ^ dependencies declared in turbo.json. It is close to zero-config for a small team and gets you caching fast.

Nx does more. It builds a full project graph, calculates exactly which projects are affected by a given change, and adds code generation for scaffolding new apps or packages consistently. It earns its complexity once your graph gets large or you need generators to keep a growing team consistent.

Remote caching is what makes either tool pay off at scale. Remote caching prevents duplicated build work across machines and CI runs when task inputs and outputs are declared correctly, according to Turborepo's remote caching docs, which also note that misconfigured inputs can serve stale outputs. Turborepo supports Vercel's remote cache or a self-hosted HTTP cache; Nx pairs its affected command with remote caching for the same effect.

  • pnpm workspaces: one lockfile, local linking, catalogs for shared version ranges.
  • Turborepo: simple task graph, fast to configure, good starting point.
  • Nx: full project graph, affected detection, and generators for larger teams.

Pick the smallest tool that gives you caching, affected detection, and clear ownership boundaries. Adding Nx to a two-app repo just to get generators you will not use is wasted complexity.

Minimal SaaS monorepo layouts and what to include for an MVP

A working starter layout looks like this: apps/web, apps/api, packages/ui, packages/db, packages/auth, and infra/config. Each app under apps/ deploys on its own; packages under packages/ exist only to be shared.

Keep four things in shared packages from day one: your types, your auth logic, your API client, and your Prisma schema. These are exactly the pieces that need to change atomically across apps, which lines up with what Nx describes as the real test for a monorepo boundary: not how many apps fit in the repo, but which changes must land together. Avoid creating a generic packages/utils folder that collects code without a clear home. It tends to become a junk drawer without ownership and fosters dependency drift.

  1. Map each app to its own CI target so apps/web and apps/api build and deploy independently.
  2. Build shared packages only when a change actually affects them, using affected detection in your task runner.
  3. Keep the auth and database packages small and typed; resist adding business logic to them.
  4. Wire deploy targets last, after the workspace and task graph both build cleanly on your machine.

For a first version, pnpm + Turborepo + Next.js + Prisma covers the ground most SaaS MVPs need without pulling in tooling you will not use for months.

Most common failures and the exact mitigations to apply

Every monorepo problem I have seen traces back to one of four causes, and each one has a known fix.

Git performance degrades as history and file count grow. Sparse checkouts, shallow clones, and archiving old history when the repo gets heavy keep day-to-day operations fast, a pattern Atlassian's monorepo guide lists among the standard mitigations for repository growth and slow Git operations.

CI waste is the second failure: running every test and build on every commit regardless of what changed. The fix is declaring task inputs and outputs explicitly and turning on affected detection and remote caching so CI only touches what a change actually reaches.

Ownership erodes without enforcement. Module-ownership files, lint rules that block cross-boundary imports, and a stated dependency direction between packages stop the repo from turning into a tangle where anyone can import anything. My guide on operational governance for growing codebases covers this in more depth for teams past the MVP stage.

Dependency drift creeps in when different packages pin different versions of the same library. A single lockfile plus pnpm catalogs, with a controlled upgrade process instead of ad hoc version bumps, closes that gap.

  • Git slowdowns: sparse checkouts, shallow clones, and history archiving as the repo ages.
  • CI waste: affected detection, remote caching, and explicit task inputs/outputs.
  • Ownership drift: module-ownership files and import boundary lint rules.
  • Dependency drift: single lockfile and pnpm catalogs with controlled upgrades.

Pro Tip: Track cache hit rate and CI run time as your main operational metric. Both should trend down as your setup matures, and a sudden spike in either usually means a task input was declared wrong.

A practical step-by-step plan to start or migrate to a monorepo safely

Start by listing your atomic-change sets before touching any tooling. Write down which files, apps, or contracts tend to change together. That list becomes your package boundaries later.

  1. Bootstrap a pnpm workspace and move one shared package in at a time, using workspace: links and catalogs instead of pinned versions.
  2. Add a task runner, Turborepo or Nx, with a minimal config, and confirm every app still builds locally before touching CI.
  3. Turn on remote caching, declare task inputs and outputs explicitly, and run one affected-only CI job before rolling it out broadly.
  4. Migrate a single app first, measure CI time and developer experience for a week, then bring in the rest of the packages.

This sequencing matters because a big-bang migration hides problems until they hit everyone at once. Doing it one app at a time means a bad caching config or a missing input declaration surfaces on one pipeline instead of all of them. My post on CI/CD patterns for B2B SaaS walks through mapping repo structure to deploy pipelines in more detail, which is worth reading before you wire up production deploys from the new structure.

Pro Tip: Run the old and new build pipelines side by side for at least one release cycle before retiring the old one. It costs a little CI time and saves you from finding out about a missing environment variable in production.

What a monorepo actually is

A monorepo is one repository holding multiple distinct projects, and each of those projects can still build, test, and deploy on its own. That last part is the piece people miss. A monorepo is a version-control strategy, not a runtime architecture, and it has nothing to do with whether your software runs as one process or twenty, a distinction Nx's definition of a monorepo makes explicit.

A monolith is a runtime decision: one application, one deployable, one process boundary. You can run a monolith out of a monorepo or a polyrepo, and you can run a dozen independently deployed microservices out of a single monorepo. Confusing the two leads teams to reject monorepos out of a fear of coupling that the repo structure itself does not cause.

What actually determines your repo boundary is which changes need to be atomic. If your billing logic, your API contract, and your frontend types have to change together to ship safely, keeping them in one repo means one commit does that instead of three coordinated pull requests across three repos. If they never change together, the case for combining them weakens regardless of how the code happens to be organized today.

Strategies for scaling a monorepo as the SaaS product grows

The first sign your monorepo needs more structure is CI time creeping up even for small changes. That is almost always a task-graph problem, not a repo-size problem: something is declared as an input to a build that does not actually affect it, so the task runner rebuilds more than it needs to.

The fix scales in stages. Early on, Turborepo's task graph and basic remote caching are enough. As the number of packages grows past what one person can track mentally, moving to Nx's project graph gives you a visual and programmatic map of what depends on what, which makes affected detection more precise as the graph gets denser.

Ownership boundaries need to scale alongside the code. A five-package repo can survive on convention; a thirty-package repo needs lint rules that physically block a forbidden import, and a module-ownership file that says who reviews changes to each package. Skipping this step is how a monorepo earns its reputation as unmanageable, when the actual cause was letting boundaries stay informal past the point where informal worked.

Splitting a package that has grown too large is a normal part of scaling, not a failure. If packages/ui has become a place where three unrelated component families live together, split it before it becomes the reason every UI change triggers a full rebuild across apps that only use one of the three.

Strategies for scaling a monorepo as the SaaS product grows — overview diagram

Best practices for versioning and release management within a SaaS monorepo

Most SaaS monorepos do not need independent semantic versions for every internal package. If packages/ui and packages/auth are only consumed by apps inside the same repo and never published externally, versioning them independently adds overhead without buying you anything: just build and deploy the apps that depend on them whenever those apps ship.

Versioning matters more when a package is published externally, say a client SDK or a shared component library other teams outside the repo consume. In that case, tag releases per package and keep a changelog scoped to that package, not the whole repo.

Release management for the apps themselves should map one to one with your deploy targets. apps/web and apps/api each get their own release process, triggered by changes affected detection actually traces to them, not by every commit to the repo. This keeps your release cadence tied to what changed, not to repo-wide activity, and it is the same principle behind treating package graphs and task graphs as distinct concepts: what depends on what is not the same question as what deploys when.

Testing approaches and test organization in a monorepo

Unit tests stay close to the code they test, inside each package or app, and run fast because they only need the affected package's build output. Turborepo or Nx should scope unit test runs the same way they scope builds: only run tests for packages actually touched by a change.

Integration tests usually live at the app level, since they exercise how a package behaves inside a real app context, for example how packages/auth behaves once wired into apps/api. Keep these separate from unit tests in your task graph so a slow integration suite does not block a fast unit test feedback loop.

End-to-end tests are the one category that resists the affected-only model well, because a change in a shared package can break a user flow in an app that never directly imports the changed file through an obvious path. Running the full end-to-end suite on changes to shared packages, even when affected detection says only one app is impacted, is a reasonable exception to make. Running it on every commit to an app-only change is usually not necessary.

Structuring tests this way keeps CI time proportional to what actually changed, which is the same principle behind caching and affected detection everywhere else in the repo.

Handling security and access controls specific to monorepos in SaaS

A monorepo does not have to mean one flat set of permissions. Most Git hosts support branch protection and required reviewers at the folder or path level, which lets you require a specific reviewer for changes to packages/auth or packages/db without splitting those packages into separate repositories.

The bigger risk is secrets and environment configuration leaking across apps that should not share them. Keep environment variables scoped per app in infra/config, never in a shared package, and never committed to the repo directly. A shared package that reads an environment variable meant for one app is a common way a monorepo quietly creates a security boundary problem that would not exist if that logic lived in the app itself.

Dependency-direction rules double as a security control, not just a code-quality one. If your lint rules block packages/ui from importing anything out of apps/api, you also block a scenario where frontend code accidentally bundles server-only secrets or database access code into a client build. This is one more reason module-ownership files and import boundary rules earn their place early rather than being treated as cleanup work for later.

How monorepos change developer productivity and team workflows

The productivity case for a monorepo is mostly about removing friction between changes that should be simple and are not. A developer who needs to update a type, the API that returns it, and the component that displays it should do that in one pull request, reviewed once, tested once. Splitting those across three repos turns one change into three, each waiting on the others to merge in the right order.

The tradeoff shows up in CI time if the tooling is not set up correctly. A team that turns on a monorepo without affected detection or caching will see every commit trigger a full rebuild of everything, which is slower than three small repos each running their own focused pipeline. The tools solve this, but only when configured, which is why affected detection and remote caching are not optional extras. They are the difference between a monorepo that speeds a team up and one that visibly slows it down within a month.

Onboarding tends to get easier in a monorepo because a new engineer can see the whole system, shared types included, in one clone instead of piecing together how three repos relate. The tradeoff is that a badly organized monorepo, with unclear boundaries and no ownership files, can be more confusing to a newcomer than three small, well-scoped repos would have been.

Examples of SaaS companies using monorepos

Nx and Turborepo both exist because real SaaS and platform teams hit the coordination problems described above and needed tooling built specifically for that scale. The tools' own documentation on affected detection and package and task graphs describes patterns pulled directly from production use: calculating the minimal set of projects touched by a change using Git diff combined with the project graph, and defining task dependencies with ^ so a build always includes what it depends on before running.

Rather than naming specific companies without a verifiable source for their setup, the more useful pattern to take away is the shape these tools were built to support: a growing number of internal packages, several deployable apps, and a need to ship changes across them without repeated dependency work or a CI pipeline that reruns everything on every commit. That is the exact problem shape a SaaS MVP hits somewhere between its first and second real customer, once the API, the web app, and the shared types start changing together often enough that coordinating across separate repos becomes the bottleneck instead of the code itself.

Hanad's short first-person notes on when I pick a monorepo for a client

I choose a monorepo when the MVP has shared contracts that need to move fast: one types package, one auth package, one API client, used by an app and an API that ship together. For a two to four week build, that setup lets me change a contract once instead of chasing it across repos.

I keep deployables independent no matter what. Scope is frozen at kickoff, so the repo structure is decided before day one, not renegotiated mid-build. My tool rule is simple: start with pnpm and Turborepo, add remote caching and affected detection once the graph earns it, and enforce ownership with lint rules from the first commit, not after the repo gets messy.

— Hanad Kubat

Bringing this into a fixed-price build

A monorepo is a structural decision, and structural decisions made under pressure at week three of a build are the ones that cause the most rework. When I scope a first real version or a rescue rebuild, the repo layout, the package boundaries, and the deploy targets get decided at kickoff, before a single feature gets built on top of them. That is what scope frozen at kickoff means in practice: no renegotiating the foundation once the work has started.

The prototype is the spec when there is one already. If a founder's Lovable, Bolt, or Bubble build validated the idea but hit a wall at the login or the payments, I use that prototype to define the atomic-change sets and shared packages the rebuild actually needs, instead of guessing at architecture from scratch. Builds start at €12,000 for Your app, and rescue rebuilds of an existing prototype run €12,000 to €20,000, both fixed price with no surprise invoices, because the work is quoted upfront and every line is written by me, no juniors.

If you want a smaller first step, the plan is a fixed-price audit at €1,500 that maps your current setup, including whether a monorepo makes sense for what you are building, and that fee is credited against the build if you move forward. You own the code from the first commit either way.

Check availability and get a fixed quote at Hanadkubat.

Bringing this into a fixed-price build — overview diagram

Sources

For implementation details straight from the source, Nx's affected command docs explain how affected detection combines Git diff with the project graph. pnpm's catalogs documentation covers the strict, prefer, and manual modes for centralizing dependency versions. For a deeper look at repo structure and CI/CD alignment, my post on multi-tenant SaaS architecture covers when to split services in ways that affect repo boundaries, and teams researching SaaS content distribution may find this guide to backlink diversity for SaaS SEO useful for a different angle on growth.

FAQ

Is a monorepo better than multiple repos for a small SaaS team?

For a small team with shared types, a shared API client, or a shared auth package, a monorepo usually reduces coordination overhead compared to multiple repos. If your apps share little code and ship on unrelated schedules, separate repositories, as Nx's monorepo versus polyrepo comparison notes, can offer cleaner isolation instead.

Do I need Nx if I am already using Turborepo?

Not necessarily. Turborepo covers task graphs and caching well for smaller setups, and Nx becomes worth adopting once you need its full project graph, generators, or more precise affected detection across a large number of packages, a tradeoff Nx's own comparison lays out directly.

Does a monorepo mean my apps deploy together?

No. A monorepo is a version-control strategy, and each app inside it can build, test, and deploy independently, which is the core distinction Nx draws in its definition of a monorepo. Keeping deploy targets separate per app is a deliberate setup choice, not something the repo structure forces either way.

What causes CI to slow down in a monorepo?

The most common cause is missing affected detection or remote caching, which means every commit rebuilds and retests everything instead of only what changed. Turborepo's remote caching documentation notes that correctly declared task inputs and outputs are what make caching actually skip unaffected work.

How do I stop dependency versions from drifting across packages?

Use a single lockfile for the whole workspace and pnpm catalogs to define shared version ranges once instead of per package. pnpm's catalogs documentation describes strict, prefer, and manual modes that let you enforce a single-version policy and cut down on merge conflicts in package.json files.