Picture a three-person startup in Bangalore building a Next.js web app, a React Native mobile app, and a Node.js API. They start with three separate repositories. Six months later, a bug in the shared date formatting logic gets fixed in one repo and forgotten in the other two. The authentication token refresh function exists in three copies, each slightly different. The component library diverges across platforms. This is the exact problem a monorepo with Turborepo solves — not by being trendy, but by making code sharing the path of least resistance. In 2026, with Turborepo mature and Vercel's remote caching free for all users, there has never been a better time to consolidate.
Why Indian Dev Teams Are Moving to Monorepos
Indian product teams — especially bootstrapped SaaS startups and IT services companies building client software — share a particular challenge: small teams maintaining multiple surfaces. A web dashboard for admins, a customer-facing Next.js app, a React Native mobile app, and a backend API often get built in separate repos because that's the default on GitHub. The cost shows up slowly: duplicated utility functions, inconsistent validation rules, UI components that drift apart, and the dreaded "it works on web but not mobile" bug caused by shared business logic existing in two different versions.
Monorepos centralize all of this. When your auth token refresh logic lives in @my-app/auth, every surface that imports it gets the fix the moment you merge it. When your design system lives in @my-app/ui, every app reflects the update automatically. The trade-off is complexity in tooling — and that's where Turborepo earns its place.
Turborepo vs Nx: When Turborepo Wins
Nx is a powerful monorepo tool with a rich plugin ecosystem, code generators, and an opinionated project structure. Turborepo is deliberately simpler. Here is when Turborepo is the right call:
- You're already on Vercel — Turborepo's remote caching integrates natively with Vercel, requiring zero additional infrastructure.
- Your team is small (2–10 engineers) — Nx's full feature set adds onboarding overhead that a small team doesn't need.
- You want to keep your existing package.json scripts — Turborepo runs your existing npm/pnpm scripts and adds caching on top. Nx often wants to own your build commands.
- Your monorepo is JavaScript/TypeScript only — Nx shines when you mix languages or need code generation. Turborepo stays focused on JS/TS.
For a typical Indian SaaS team using Next.js, React Native, and a Node API — Turborepo with pnpm workspaces is the pragmatic choice. You can always migrate to Nx later if you outgrow it.
Setting Up: apps/ + packages/ Pattern with pnpm Workspaces
The standard Turborepo structure separates runnable applications from shared code:
my-app/
├── apps/
│ ├── web/ # Next.js web app
│ ├── mobile/ # React Native (Expo)
│ └── api/ # Node.js / Express / Hono
├── packages/
│ ├── ui/ # Shared component library
│ ├── db/ # Prisma schema + generated client
│ ├── config/ # TypeScript + ESLint configs
│ └── api-client/ # Type-safe API client (fetch wrapper)
├── pnpm-workspace.yaml
├── turbo.json
└── package.json
Your pnpm-workspace.yaml tells pnpm where to find packages:
packages:
- "apps/*"
- "packages/*"
Each package in packages/ has its own package.json with a scoped name like @my-app/ui. Apps declare them as dependencies using the workspace protocol:
# apps/web/package.json
{
"dependencies": {
"@my-app/ui": "workspace:*",
"@my-app/db": "workspace:*",
"@my-app/api-client": "workspace:*"
}
}
Run pnpm install from the root and pnpm links all workspace packages together without publishing to npm.
turbo.json: Build and Test Pipelines with Caching
The turbo.json file is where Turborepo's power lives. It defines the task dependency graph and what outputs to cache:
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**", "build/**"]
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"]
},
"lint": {
"outputs": []
},
"dev": {
"cache": false,
"persistent": true
}
}
}
The ^build in dependsOn means "run build in all dependencies first." So when you build apps/web, Turborepo automatically builds @my-app/ui, @my-app/db, and @my-app/api-client first — in the right order. The outputs array tells Turborepo what to cache. If your source files haven't changed, the next build restores from cache instead of rerunning.
Remote Caching with Vercel: Saving 90% CI Time
Local caching helps on your machine. Remote caching is what makes CI fast for the whole team. When your GitHub Actions runner pulls the Turborepo cache from Vercel's servers, a cold build that takes 14 minutes becomes a 90-second cache restoration for unchanged packages.
Setup requires two steps. First, authenticate Turborepo with your Vercel token:
npx turbo login
npx turbo link
This creates a .turbo/config.json with your team ID. Second, add the Vercel token to your GitHub Actions secrets and pass it to the build step:
# .github/workflows/ci.yml
- name: Build
run: pnpm turbo run build test lint
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
Vercel's remote cache is free for all Turborepo users as of 2026. If you need full control over cache storage — for compliance reasons common with Indian enterprise clients — you can self-host a Turborepo-compatible cache server using community implementations like ducktape.
Building the Shared Packages
The four packages that pay off immediately in a typical Indian SaaS monorepo:
@my-app/ui — Shared Component Library
The most impactful package for web-heavy teams. Build it around shadcn/ui if your design language is flexible, or wrap your custom Figma components. The key is exporting components as TypeScript with full prop types — this gives you autocomplete across every app that consumes the package. Configure packages/ui/package.json to point its exports field at the compiled output, not the source:
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
}
}
}
@my-app/db — Prisma Schema and Generated Client
Put your Prisma schema here along with a database client singleton. Every app that needs database access imports from @my-app/db — no duplicated Prisma clients, no schema drift. The package's build step runs prisma generate, ensuring types are always current after pnpm turbo run build.
@my-app/config — TypeScript and ESLint Configurations
Shared TypeScript and ESLint configs eliminate the "it builds on my machine" problem. Create a base tsconfig.json in this package and extend it everywhere:
// apps/web/tsconfig.json
{
"extends": "@my-app/config/tsconfig/nextjs.json",
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
}
@my-app/api-client — Type-Safe API Client
Generate or hand-write a typed fetch wrapper that both web and mobile apps import. When your API adds a new endpoint, update the client package and TypeScript will surface every call site that needs updating. Pair this with Zod schemas in the same package to share request/response validation between your API and frontends.
Making Packages Work with Next.js App Router and Vite Simultaneously
The trickiest part of a multi-app monorepo is packages that need to work with both Next.js (which uses its own bundler) and Vite (common for React-only admin dashboards). Two patterns work well:
Option 1: Build-first packages. Compile shared packages to ESM before Next.js or Vite consume them. Add a build script to each package that runs tsup or unbuild, and declare dependsOn: ["^build"] in turbo.json. Next.js and Vite then import compiled JavaScript, avoiding transpilation conflicts entirely.
Option 2: Transpile in the consumer. For Next.js, use transpilePackages in next.config.js to tell Next.js to treat workspace packages as source. For Vite, this happens automatically. This approach skips the compile step in shared packages, which is simpler but can cause issues with packages that use features unsupported by one bundler.
The build-first approach is more reliable for production monorepos, especially when React Native (which uses Metro bundler) is in the mix.
CI/CD on GitHub Actions: Only Rebuild Affected Packages
With remote caching configured, your GitHub Actions workflow becomes straightforward:
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with: { version: 9 }
- uses: actions/setup-node@v4
with: { node-version: 22, cache: pnpm }
- run: pnpm install --frozen-lockfile
- run: pnpm turbo run build test lint
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
Turborepo handles the "only rebuild what changed" logic automatically using its task graph and file hashing. If you push a change to packages/ui, Turborepo rebuilds @my-app/ui and every app that depends on it, but skips the API server and any packages that don't consume UI components. You don't write any conditional logic — the dependency graph in turbo.json drives everything.
Common Issues: Circular Dependencies, Path Aliases, and Hot Reload
Circular Dependencies
Turborepo detects cycles and refuses to build. The usual culprit is two packages importing from each other. Extract the shared piece into a third package — often @my-app/types or @my-app/shared — that neither side imports back from. Hidden cycles from TypeScript path aliases are more insidious: audit your tsconfig.json paths to make sure they respect the package graph direction.
TypeScript Path Aliases Across Package Boundaries
Don't use @/* aliases inside shared packages. They resolve relative to the consuming app's tsconfig, not the package's tsconfig, which produces confusing "module not found" errors. Inside packages, use relative imports (../utils/format) or properly configured exports in package.json. Reserve path aliases for within apps only.
Hot Reload in Development
If you're using build-first packages, changes to shared packages won't hot-reload in your app automatically because the app is watching compiled output. Two approaches: run pnpm turbo run dev --parallel with a watch mode build in each package (add "dev": "tsup --watch" to package scripts), or switch to Option 2 (transpile-in-consumer) for development. Many teams use build-first for CI/prod and transpile-in-consumer during local dev by toggling transpilePackages.
Practical Example: Indian SaaS with Next.js Web and React Native Mobile
Consider an edtech startup in Kochi building a student dashboard in Next.js and a parent monitoring app in React Native. Their monorepo structure after six months of real usage:
- apps/web — Next.js 15 App Router, uses
@my-app/ui,@my-app/db,@my-app/api-client - apps/mobile — Expo SDK 52, uses
@my-app/api-client,@my-app/types(no UI sharing — mobile has its own native components) - apps/api — Hono on Node.js, uses
@my-app/db,@my-app/types - packages/ui — Web-only components built on Radix + Tailwind
- packages/db — Prisma + PostgreSQL (hosted on Supabase, Mumbai region)
- packages/types — Shared Zod schemas, TypeScript interfaces, enum constants
- packages/api-client — Typed fetch client generated from the Hono router's type exports
Their CI went from 22 minutes (three separate pipelines) to 4 minutes average (Turborepo with Vercel remote cache). The critical win: a timezone bug in student attendance calculation was fixed once in @my-app/types and propagated to web, mobile, and API in a single PR. Previously this same class of bug had been fixed in the web app, missed in mobile, and surfaced in a parent complaint two weeks later.
Frequently Asked Questions
Can Turborepo share packages between a Next.js app and a React Native mobile app simultaneously?
Yes, but with one important constraint: packages that contain JSX or browser-specific code need separate entry points for web and native. The cleanest approach is to publish your shared package with a platform field in package.json pointing to a native/ subdirectory. Logic-only packages — validation schemas, API clients, TypeScript types, date utilities — share without any modification. UI components need a web/ and native/ split, which you can manage through Turborepo's package boundary without duplicating business logic.
How much does Turborepo remote caching actually save on GitHub Actions, and is Vercel's remote cache free?
In practice, remote caching reduces CI times by 60–90% on established monorepos where most packages haven't changed between commits. For a monorepo with 8 packages and a 12-minute cold build, you can expect 2–4 minutes on cache-hit runs. Vercel's remote cache is free for all Turborepo users — you authenticate with your Vercel account token and point turbo.json at Vercel's cache API. There is no storage limit published for the free tier as of early 2026, though Vercel reserves the right to change this. Self-hosted caching alternatives exist via open-source Turborepo Cache Server implementations if you need full control.
What causes circular dependency errors in Turborepo and how do you fix them?
Circular dependencies in Turborepo almost always originate from packages importing each other — for example, @my-app/ui importing from @my-app/db, while @my-app/db also imports @my-app/ui for a UI helper. Turborepo will refuse to build and print a cycle detection error. The fix is to extract the shared piece into a third package (@my-app/shared or @my-app/types) that neither side imports back from. A common source of hidden cycles is TypeScript path aliases in tsconfig.json that bypass the package graph — audit your tsconfig paths against your pnpm-workspace.yaml package list to make sure aliases only point in one direction.