Monorepo for Indie SaaS: Defer It | Coding Capybaras

Monorepo or single app for your indie SaaS? Why solo founders should default to one app, the signals that justify splitting, and how to keep the door open.

· Justin Boggs

Railway track switches with yellow levers in daylight, tracks diverging in two directions

Photo by pavel ondera on Unsplash

For a monorepo on an indie SaaS built by one person, the honest answer is: not yet — build a single app. Not because monorepos are bad — they're excellent, and you'll probably want one eventually — but because a monorepo solves coordination problems between teams and deploy targets, and you currently have one of each. The good news is that this is one of the cheapest architecture decisions to defer. If you organize your single app with clear internal boundaries now, converting to a monorepo later is a mechanical move, not a rewrite. This post covers when the switch is actually worth throwing, and how to keep it easy to throw.

TL;DR

  • A monorepo is multiple projects in one Git repository — it is not the same thing as a monolith. Repo layout and deployment architecture are independent choices.
  • The problems monorepos solve are coordination problems: shared code across apps, atomic cross-project changes, one dependency policy. A solo founder with one app has none of them yet.
  • The real cost of a premature monorepo is the tooling tax — workspace config, task orchestration, build caching, and a directory structure you have to navigate every day.
  • Four signals justify splitting: a second deployable surface, a genuinely shared package, a native mobile app, or a second person with a different release cadence.
  • Buy the option cheaply: enforce internal boundaries with path aliases and import rules now, and the migration later is mostly moving folders.

What a monorepo actually is (and what it isn't)

Start by killing the most common confusion, because it changes the whole conversation.

A monorepo means multiple projects live in one Git repository. A monolith means one tightly-coupled application. These are unrelated. Vercel's monorepo course states it plainly: you can run microservices out of a monorepo, and you can split a monolith across several repositories. Google does the former at enormous scale.

So when someone tells you "monorepos don't scale" or "monorepos are what big companies do," ask which of the two things they mean. Usually they're arguing about deployment and using the wrong word.

In practice, a JavaScript monorepo has a recognizable shape:

your-product/
├── apps/                  # things you deploy
│   ├── web/               # the Next.js app
│   └── docs/              # a separate docs site
├── packages/              # shared code, not deployed on its own
│   ├── ui/                # component library
│   └── config/            # shared tsconfig, eslint
├── package.json           # root scripts
├── pnpm-workspace.yaml    # which folders are workspaces
└── turbo.json             # task orchestration

Four moving parts make that work. A package manager with workspace support — pnpm has this built in — so packages/ui can be imported by apps/web without publishing to npm. A workspace manifest declaring which directories count. A task runner like Turborepo or Nx that knows which packages changed and only rebuilds those. And a build cache, local and usually remote, so unchanged packages replay instead of recompiling.

None of that is hard. All of it is something. Every one of those files is a thing you configure, a thing that can break, and a thing you have to understand when a build fails at 11pm.

Now compare that to what a single Next.js app needs to deploy: a package.json and a git push.

The problems a monorepo solves — and whether you have them

Nx's comparison of monorepos and polyrepos frames the tradeoff well: with modern build tooling, the technical challenges are mostly solved, so the choice is primarily organizational — how teams want to share code, review changes, and release.

Read that word again. Organizational. Here's their breakdown of the dimensions, with an honest column for where a solo founder actually sits:

| Dimension | Monorepo | Separate repos | Solo founder, one product | | --- | --- | --- | --- | | Code sharing | Import directly from the same repo | Publish versioned packages | Nothing to share yet — it's one app | | Atomic changes | One PR updates an API and every consumer | Changes span multiple PRs and releases | One PR, because there's one project | | Release cadence | Everything builds from the same commit | Independent by default | One cadence: whenever you push | | CI cost | One pipeline, needs tooling to run only what changed | Many small pipelines to maintain | One pipeline, ~2 minutes | | Access control | Everyone sees everything, per-folder ownership | Repo-level permissions per team | You | | Dependency versions | A single policy is enforceable | Versions drift across repos | One package.json | | Team autonomy | Shared conventions | Each team picks its own | Not applicable |

Six of those seven rows resolve to "not a problem I have." That's not a knock on monorepos — it's evidence that the tool is aimed at a situation you're not in.

The one row that is live for a solo founder is code sharing, and it's worth being precise about it. You don't have shared code because you have two apps. You'd have shared code if you built a second app. That's a future condition, not a current one, and building the machinery for it now is the definition of premature.

There's one genuinely new consideration in 2026 that didn't exist a few years ago, and it cuts in the monorepo's favor: AI agents. As Nx puts it, an agent working in one repository can't see the consumers of the code it's changing, and agents work best when they can see every project affected by a change. If you're building with Claude Code or Cursor — and if you're reading this, you probably are — that's a real argument for keeping related code in one place.

But notice what it argues for. It argues against splitting into separate repos. A single app is already one place. On the AI-context axis, a single app and a monorepo are on the same side of the line.

The tooling tax nobody quotes you

Here's what actually goes wrong when a solo founder adopts a monorepo too early. None of it is dramatic. All of it is friction, and friction is what kills side-project momentum.

Every path gets longer. apps/web/src/components/pricing-table.tsx instead of components/pricing-table.tsx. You feel this every time you or your AI assistant references a file, and AI assistants get file paths wrong often enough already without adding two levels of nesting.

Dependency questions get a new dimension. Does this package go in the root, or in apps/web, or in packages/ui? There are real answers, but now you're answering them instead of shipping. Hoisting behavior differs between package managers, which is a sentence you didn't need in your life at month two.

Builds get a config surface. Turborepo or Nx needs to know your task graph — what depends on what, what's cacheable, what outputs matter. When it's right it's genuinely fast. When it's subtly wrong you get stale cache hits, which produce the worst class of bug: the one where your code is correct and the output isn't.

Deploy config gets a root directory. Vercel handles monorepos well, but you're now setting root directories per project and reasoning about which pushes trigger which builds.

Your AI assistant needs more orientation. Every session, it has to understand the workspace layout before it can help. That's context spent on structure rather than on your actual problem.

Each of these is maybe fifteen minutes. Together, on a project where you're the only person and your scarcest resource is uninterrupted evenings, they add up to a meaningful tax on a benefit you're not yet collecting. This is the same argument I made for choosing boring technology: the exciting option usually isn't wrong, it's just early.

The four signals that mean it's time

Defer isn't the same as never. Here's what actually flips the decision. When one of these is true — not anticipated, true — split.

Signal 1: a second thing you deploy separately. A marketing site that needs to build independently from the app. A docs site on its own domain. A public status page. The moment you have two deploy targets that share components, you have the coordination problem monorepos exist to solve.

Watch for the false positive here. A /blog route and a /pricing route inside your Next.js app are not a second deploy target. They ship with everything else. Same repo, same build, no monorepo needed.

Signal 2: code you've now copy-pasted twice. Not code you think you'll share — code you have actually duplicated. When the same Button or the same Stripe helper exists in two places and you've already fixed a bug in one and forgotten the other, that's a shared package asking to exist.

Signal 3: a second runtime. A React Native app, a CLI, a background worker service with different dependencies. Different runtimes with shared types and business logic are the strongest single case for a workspace, because the alternative is publishing private npm packages, which is worse.

Signal 4: a second person with a different cadence. Once someone else is shipping on a schedule that isn't yours, the questions Nx lists — dependency policy, code ownership, CI ownership, deployment coordination — become real questions with real answers. A monorepo makes those answerable in one place.

If none of these is true, you're doing the right thing by waiting. And notice that all four are observable events, not judgment calls. You'll know.

One thing that is not a signal: the codebase feeling large. Size alone doesn't create the coordination problems a monorepo solves. A 40,000-line Next.js app with one deploy target and one developer has exactly the same coordination profile as a 4,000-line one — bigger, but not structurally different. If a big single app feels unmanageable, the fix is usually clearer internal boundaries, not a workspace. Splitting a tangled codebase across packages doesn't untangle it; it just spreads the tangle across more directories and adds version numbers to it.

Nor is "I might want to open-source part of this later." Extracting a package for publication is its own job, and you can do it from a single app the same day you decide to.

How to keep the door open (this is the actual work)

Deferring a decision is only smart if deferring stays cheap. Here's how you make sure it does, and it comes down to one idea: enforce logical boundaries now, even inside one app.

The mistake isn't picking a single app. It's picking a single app and letting everything import everything, so that four months later the code has no seams and extracting a shared package means untangling three hundred imports.

Coding Capybaras runs this pattern. It's one Next.js app, one repo, one deploy — but the source is split into three regions with rules between them:

flowchart TD
    subgraph one["One Next.js app, one repository"]
        A["/app/<br/>routing shims only"]
        B["/platform/<br/>auth, billing, email, config<br/>shared infrastructure"]
        C["/website/<br/>marketing, pricing, blog"]
        D["/product/<br/>the app behind sign-in"]
    end
    A --> B
    A --> C
    A --> D
    C -->|"@/platform/*"| B
    D -->|"@/platform/*"| B
    B -.->|"not allowed"| D

Three rules make that stick, and none of them require monorepo tooling:

Path aliases instead of relative imports. Every cross-region import goes through @/platform/*, @/website/*, or @/product/* — never ../../../lib/auth. This is one tsconfig.json change. It matters more than it looks: aliases are what make a later move-to-packages/ a find-and-replace rather than an archaeology project.

A stated dependency direction. In our case, product code may import from platform, never the reverse. Write the rule down; an ESLint boundary rule can enforce it if you want teeth. A dependency graph that only points one way is a graph you can cut.

A documented public surface. We keep an INTERFACES.md listing what each region exports. It sounds like ceremony for a one-person project. In practice it's the file that stops you from reaching into someone else's internals at 1am, and it's the first thing an AI assistant reads to figure out what it's allowed to call.

There's a fourth boundary worth drawing that people forget, because it isn't in the import graph: the database. We prefix every infrastructure table platform_ and every product table app_, and product code is not allowed to query platform_* tables directly — it goes through the documented helpers instead. A naming convention is a weak enforcement mechanism, but it's a strong signal, and it means a query that crosses a boundary is visible in a diff rather than buried in a join. If you ever split the app, the schema splits along the same line you already drew — which is worth more than it sounds, because schema decisions are the ones that are genuinely painful to reverse later.

The nice thing about all four rules is that they're free. There's no package to install and nothing to configure. You're spending attention, not build time, and you're spending it on the part of the decision that's actually hard to undo. Workspace tooling can be added in an afternoon at any point in your project's life. A codebase where every module imports every other module cannot be un-tangled in an afternoon at any point in your project's life.

Do those three things and the eventual migration looks like: move platform/ to packages/platform, add a pnpm-workspace.yaml, update the alias targets, add turbo.json. An afternoon. Skip them, and the migration is a refactor you'll postpone forever, which is how people end up with a monorepo-shaped ambition and a spaghetti-shaped repo.

This is the same logic as deciding what to build versus what to buy: the goal isn't to make the perfect call today, it's to avoid making today's call expensive to revisit.

Frequently asked questions

Is a monorepo the same thing as a monolith?

No, and conflating them causes most of the confusion in this debate. A monorepo is about where code lives in version control; a monolith is about how an application is architected and deployed. You can deploy independent services from one monorepo, or split a single tightly-coupled app across several repositories.

Can I use Turborepo with just one app?

Yes — Turborepo supports single-package workspaces and will still cache your tasks. Whether it's worth adding depends on your build times. If pnpm build finishes in under two minutes, the caching upside is small relative to the config you're taking on.

How hard is it to convert a single app to a monorepo later?

Mostly mechanical, if you kept internal boundaries clean. You move directories into packages/, add a workspace manifest, and update import aliases. The hard version of this migration is caused by tangled imports, not by the monorepo tooling — which is why the boundary discipline above is the real investment.

Does a monorepo make my AI coding assistant better or worse?

Better than separate repos, roughly neutral versus a single app. The advantage is context: an agent can see every project affected by a change, which it can't do across repository boundaries. The disadvantage is that deeper directory nesting gives it more chances to reference a path that doesn't exist.

What about putting my marketing site in a separate repo?

That's the polyrepo option, and it's the one I'd avoid earliest. You get the coordination costs of two repos — duplicated CI, duplicated dependency upgrades, no shared components — without the code-sharing benefit of a workspace. If the marketing site is truly independent, a route group in the same app is simpler; if it needs its own deploy, that's Signal 1 and you want a monorepo, not a second repo.

Should I structure my folders like a monorepo without actually being one?

That's essentially what the region-split approach does, and it's a good middle path. Just don't fake it with a packages/ directory that isn't wired to a workspace — either use real workspace tooling or use plain directories with enforced import rules. The half-configured version confuses both humans and AI assistants.

The decision, restated

If you're one person shipping one product, build one app. Spend the time you'd have spent on workspace configuration on internal boundaries instead — path aliases, a stated dependency direction, a written list of what each region exports. That work pays off immediately in a codebase you and your AI assistant can navigate, and it pays off again later by making the monorepo migration an afternoon instead of a project.

Then watch for the four signals. A second deploy target, code you've duplicated twice, a second runtime, or a second person on a different cadence. When one shows up, throw the switch — a monorepo is the right destination for a growing indie SaaS, just not the right starting point. Until then, the best architecture decision available to you is the one you don't have to make yet.

The Coding Capybaras boilerplate ships with this exact structure — one Next.js app, three enforced regions, path aliases between them — if you want a working example of the boundaries rather than a description of them.

Sources