Skip to main content

Monorepo vs Single Repo: Architecture Choices in SaaS Boilerplates

·StarterPick Team
monorepoturborepoarchitectureboilerplate2026

TL;DR

Single repo (regular Next.js) for solo founders, early-stage SaaS, and products with one frontend. Monorepo (T3 Turbo or Turborepo) when you have multiple apps sharing code — web + mobile + marketing site, or web + API + workers. Most successful solo SaaS products start single-repo and evolve to monorepo only when the code-sharing problem actually exists.

What "Monorepo" Actually Means

A monorepo is a single Git repository containing multiple packages or apps:

my-saas/                     ← Single repo (just a Next.js app)
└── src/

my-saas-monorepo/            ← Monorepo (multiple apps + packages)
├── apps/
│   ├── web/                 ← Next.js web app
│   ├── mobile/              ← React Native (Expo)
│   └── marketing/           ← Astro or another Next.js app
├── packages/
│   ├── ui/                  ← Shared component library
│   ├── api/                 ← tRPC router (shared between apps)
│   ├── db/                  ← Prisma schema + client
│   └── config/              ← Shared TypeScript, ESLint config
└── turbo.json

The monorepo pattern exists to solve the code-sharing problem: when you have multiple deployable apps that need to share types, components, or business logic.

Single Repo: Simple, Fast, Focused

A standard Next.js app in a single repo is what most boilerplates ship:

starterpick-single/
├── app/                  ← Next.js App Router
│   ├── (auth)/
│   ├── (dashboard)/
│   └── api/
├── components/
├── lib/
├── prisma/
└── package.json

Advantages:

  • Zero tooling overhead (no Turborepo, no workspace configs)
  • Fast npm install, simpler CI
  • Every developer understands the structure immediately
  • Easy to deploy on Vercel with zero config

Disadvantages:

  • Code is tightly coupled — if you add mobile later, you copy code
  • Marketing site lives in the same deploy as the app
  • API and frontend can't be deployed separately

T3 Turbo is the most popular monorepo boilerplate. Built on Turborepo with TypeScript, tRPC, and Next.js.

t3-turbo/
├── apps/
│   ├── nextjs/           ← Main web app
│   └── expo/             ← React Native mobile
├── packages/
│   ├── api/              ← tRPC router
│   ├── auth/             ← NextAuth config
│   ├── db/               ← Prisma schema + client
│   └── validators/       ← Zod schemas shared between apps
├── turbo.json
└── package.json

The Code Sharing Pattern in Practice

// packages/api/src/router/post.ts
// This router runs on BOTH web and mobile — defined once
import { z } from 'zod';
import { createTRPCRouter, protectedProcedure } from '../trpc';

export const postRouter = createTRPCRouter({
  create: protectedProcedure
    .input(z.object({ content: z.string().min(1).max(500) }))
    .mutation(async ({ ctx, input }) => {
      return ctx.db.post.create({
        data: { content: input.content, authorId: ctx.session.userId },
      });
    }),

  feed: protectedProcedure.query(async ({ ctx }) => {
    return ctx.db.post.findMany({
      where: { authorId: ctx.session.userId },
      orderBy: { createdAt: 'desc' },
      take: 20,
    });
  }),
});

// apps/nextjs/src/app/dashboard/page.tsx
import { api } from '@/trpc/server';  // Server-side caller

// apps/expo/src/screens/feed.tsx
import { api } from '@acme/api/native';  // React Query on mobile

Same router, same types, same validation — no code duplication.

Turborepo Build Caching

// turbo.json — parallel builds with smart caching
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],  // Build packages before apps
      "outputs": [".next/**", "!.next/cache/**", "dist/**"]
    },
    "typecheck": {
      "dependsOn": ["^typecheck"]
    },
    "lint": {},
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

Turborepo caches build outputs. If packages/db didn't change, it doesn't rebuild. On a large monorepo, this saves 30-90% of CI time.


The Real Trade-Offs

Monorepo Adds Overhead

Setting up a monorepo correctly takes effort:

// packages/ui/package.json — every shared package needs this
{
  "name": "@acme/ui",
  "version": "0.0.1",
  "exports": {
    ".": {
      "import": "./src/index.tsx",
      "types": "./src/index.tsx"
    }
  },
  "scripts": {
    "build": "tsc",
    "lint": "eslint .",
    "typecheck": "tsc --noEmit"
  }
}

Every shared package needs its own package.json, export maps, TypeScript config, and lint config. Small issue, but multiplied across 5-10 packages.

CI Becomes More Complex

# Single repo CI — simple
- run: npm install
- run: npm run build
- run: npm run test

# Monorepo CI — need to be smart about what changed
- run: npm install
- run: npx turbo build --filter=[HEAD^1]  # Only build affected packages
- run: npx turbo test --filter=[HEAD^1]

Turborepo's --filter flag limits builds to changed packages. You need to understand this to avoid slow CI.


Decision Framework

Choose single repo if:

  • Solo founder or small team (1-3 devs)
  • One frontend (web only)
  • Moving fast and iterating on product
  • No immediate plans for mobile
  • Deploying on Vercel

Choose monorepo if:

  • You're building web + mobile from day one
  • Multiple teams working on different parts
  • Need a shared design system/component library
  • Marketing site and app need separate deploys
  • You expect the codebase to grow significantly

The honest rule: Don't add monorepo complexity until you feel the code-sharing pain. If you're copying types between two projects, it's time for a monorepo. If you're not, you don't need one yet.


OptionStackBest For
T3 TurboNext.js + Expo + tRPCWeb + mobile with shared API
NxAny stackEnterprise teams, complex build graphs
Turborepo standaloneAny stackTeams who want flexibility without T3 opinions
pnpm workspacesAny stackMinimal tooling, just npm workspaces

Starting Single, Going Monorepo Later

You can extract to a monorepo later if you start single:

# Create new monorepo
mkdir my-saas-mono && cd my-saas-mono
npx create-turbo@latest --example with-prisma

# Move your existing app
mv ../my-saas apps/web

# Extract shared packages
mkdir packages/db
mv apps/web/prisma packages/db/
# ... update import paths

It's work, but not catastrophic. Most successful SaaS products make this transition at Series A or when adding mobile.


Find monorepo and single-repo boilerplates on StarterPick.

Check out this boilerplate

View T3 Turbo on StarterPick →

Comments