Estalio
← All insights

Splitting a Next.js app into a monorepo without breaking prod

2026-06-02·7 min read

At some point most successful single-codebase projects need to grow. A second app appears — usually a native client, an admin console, or a marketing site that's outgrown being a route in the main app. The shape that solves this with the least new infrastructure is a pnpm workspace.

We did this last week for an internal project — promoted a single Next.js repo to apps/api + packages/shared + a future apps/native. The whole refactor took under an hour and didn't break a live Stripe webhook. Here's what worked.

Decisions to make first

Three calls before touching code:

1. Package manager. If you're starting fresh, pnpm. It handles workspaces with the least ceremony, the symlink layout is straightforward, and Vercel auto-detects it. 2. Tooling depth. Skip Turborepo and Nx for now. A clean pnpm workspace is enough for two to four apps. Bring in Turborepo when build caching starts to matter — usually past six apps. 3. Shared-package shape. Decide whether the shared package builds (TypeScript compiled to JS in dist/) or ships source. For internal-only consumption inside a workspace, ship source. Less infrastructure, no build step, and Next.js's transpilePackages handles the rest.

The order of operations

Doing it in the right order matters because each step verifies the previous one:

1. Hoist the workspace. Add a pnpm-workspace.yaml at the repo root, and a minimal root package.json with packageManager: pnpm@x.y.z. 2. Move the existing app into apps/api/ using git mv. Use git mv for tracked files and plain mv for ignored ones (Next's next-env.d.ts, .next/, .vercel/, etc.). git mv preserves blame, which matters more than people think. 3. Pull shared types and pure functions into packages/shared/src/. Anything framework-coupled (Next-only cookies, Supabase SSR helpers) stays in the app. 4. Update imports. sed works. Run a focused find-and-replace, then tsc --noEmit to confirm. 5. Verify the build. pnpm --filter api build should pass cleanly. If it doesn't, fix it before doing anything else. 6. Then, and only then, update Vercel's Root Directory to apps/api in the dashboard. Without this, the next push deploys nothing and your prod paywall breaks.

That last point is the one teams skip and regret. Make the dashboard change *before* the next push.

What stayed simple

  • packages/shared exports ./src/index.ts directly. No build step. Next.js's transpilePackages array handles the on-the-fly compilation.
  • tsconfig.json in each app extends a base config or, for very small monorepos, just stands alone. We didn't bother with a shared base.
  • No path aliases at the workspace level. The @/ alias inside each app stays scoped to that app.

What needs care

  • Turbopack's workspace root. See the other post — pin it explicitly.
  • .gitignore patterns. Patterns rooted with a leading slash (/node_modules, /.next/) won't match in subdirectories. De-anchor them.
  • Lockfile. Delete package-lock.json if you're switching from npm. The first pnpm install at the workspace root generates pnpm-lock.yaml covering all packages.
  • Stripe / payment webhooks in production. Test the deploy chain end-to-end with a small commit before assuming everything is fine.

The payoff

Forty minutes of work, and what we got back:

  • One pnpm install for the whole repo.
  • Shared types between the web app and the new native app, with no copy-paste.
  • A cleaner mental model: this code is API-only, that code is native, this code is shared.

Future-self thanks present-self. Worth doing before the second app gets too big to be tidy.