2026-05-29
Monorepos for a small SaaS: when they pay off and what to skip
When pnpm workspaces (and optional Turborepo) help a single product team and what not to configure on day one.
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 runpays off. - You must coordinate breaking API changes with consumers in one revision.
Skip on day one
- Empty packages (“future-proof”
@acme/typeswith 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).
outputsinturbo.jsonare 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:
pnpm install --frozen-lockfilepnpm lint+pnpm test(parallel friendly)pnpm buildoptionally scoped via--filterto changed packages (git diffscripts)
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.