Blog

Manuel Tardivo

Monorepos for a small SaaS: when they pay off and what to skip

When a pnpm workspace (and optional Turborepo) pays off for a single product team, and what not to configure on day one of a small SaaS.

Cover image for the article “Monorepos for a small SaaS: when they pay off and what to skip”

A monorepo (pnpm workspace:*, optionally Turborepo on top) centralizes tooling pins. For one SaaS with 1–3 deployables it can still be overkill if you split packages before the boundaries are real.

Technical refs: pnpm workspaces, Turborepo caching.

When it actually helps

  • You ship multiple artifacts (api + web) that share TS types, Zod schemas, or generated clients.
  • CI burns minutes on duplicate installs / non-cached builds—remote cache or turbo run pays off.
  • You must coordinate breaking API changes with consumers in one revision.

Skip on day one

  • Empty packages (“future-proof” @acme/types with no consumers).
  • Turbo pipelines fanning lint across unrelated folders with near-zero cache hits.
  • Independent semver for internal libs if you always deploy one atomic release—prefer workspace:* + git tags.

Turbo cache ROI

Remote cache wins when:

  • Tasks are deterministic (same inputs → same outputs).
  • outputs in turbo.json are accurate—otherwise you get poisonous hits.

For 1–2 dev teams, sometimes pnpm --filter web build + CI artifact cache is enough without Turbo.

Minimal turbo.json (commented)

Reasonable starting point for two packages web and api that emit dist (rename to match your package.json names):

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".output/**"]
    },
    "lint": {
      "outputs": []
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs": ["coverage/**"]
    }
  }
}
  • dependsOn: ["^build"] — build upstream workspace deps first (shared types, etc.).
  • outputs — honest list of generated dirs; if wrong, remote cache silently restores stale artifacts.

Typical local run: pnpm turbo run build --filter=web or turbo run build from the root when every package should participate.

Minimal CI

Typical matrix:

  1. pnpm install --frozen-lockfile
  2. pnpm lint + pnpm test (parallel friendly)
  3. pnpm build optionally scoped via --filter to changed packages (git diff scripts)

Avoid duplicating identical jobs per micro-package when total build time stays under a few minutes.

Remote caching and Turbo

If you adopt Turborepo, the inflection point is usually remote caching: reuse dist/ artifacts and task outputs across CI machines instead of rebuilding cold every time. Without remote cache, Turbo is still a solid local orchestrator but barely moves CI minutes. Declare outputs in turbo.json honestly—lying to the cache poisons builds silently.

When to stay monolithic

Signals:

  • You spend more time bumping internal versions than shipping features.
  • Cross-package debugging hurts more than the structural win.

A tidy monolith with folders (apps/web, services/api) and one or two package roots is perfectly professional until deploy boundaries diverge.

Summary

Adopt the monorepo when deploy boundaries + real shared code exist. Otherwise keep a disciplined monolith—less tooling, same rigor.

Portrait of Manuel Tardivo

Co-founder & CEO, Snowinch

Manuel Tardivo

On LinkedIn