Most production software is not one folder and one package.json. It is a marketing site, a dashboard, shared database types, a Python MCP server, and a folder of agent skills—all shipping together. A monorepo (monolithic repository) keeps those pieces in one Git tree so you can change an API and every consumer in the same commit.
This guide is Monorepos 101: definitions, when they help, how JavaScript, Next.js, and Python setups differ, and how AI agents and MCP servers fit the layout—with references and copy-paste examples. Tooling stats and version notes below were checked against primary sources (Turborepo, Nx, MCP TypeScript SDK, monorepo.tools) in May–June 2026.
Key takeaways
-
Monorepo ≠ monolith. You still deploy separate apps; you version them together.
-
Workspaces (npm, pnpm, Yarn, Bun) wire local packages with
workspace:*dependencies—foundation of JS monorepos. -
Orchestrators (Turborepo, Nx, Bazel) add caching and task graphs; optional until CI time hurts.
-
Next.js apps are usually
apps/web-style packages that depend on sharedpackages/*libraries. -
Python often lives in parallel (
externals/,services/) with Poetry or uv workspaces—not merged into npm. -
Agent skills are repo-level instructions (
.agents/skills); MCP servers are separate processes agents call at runtime. -
Trade-off: faster refactors and shared CI vs. repo size, permission complexity, and slow builds without caching.
TL;DR
| Question | Answer |
|---|---|
| What is it? | One repo, many packages/projects |
| Why use it? | Atomic changes, shared types/tools, one CI pipeline |
| JS minimum | Root package.json + workspaces + internal packages |
| Next.js pattern | apps/web imports packages/db, packages/ui, etc. |
| Python pattern | pyproject.toml per service or uv workspace at root |
| Agents | Skills at repo root; harness reads them per session |
| MCP | Standalone TS/Python package; configured in Cursor/Claude, not bundled into Next |
| Learn more | pnpm workspaces, Turborepo docs, uv workspaces, monorepo.tools |
What is a monorepo?
A monorepo is a version-control strategy: multiple software projects live in one repository, typically with:
- A root that defines tooling (lint, test, CI)
- Apps — deployable surfaces (
apps/web,apps/api) - Packages — shared libraries (
packages/db,packages/ui) - Sometimes tools, docs, infra, and externals (vendored or git-subtree code)
The term comes from contrast with polyrepo (many repos, one product per repo). Google’s public engineering literature and tools like Bazel popularized monorepos at extreme scale; startups and mid-size teams often use npm/pnpm workspaces + Turborepo for a lighter version of the same idea.
Nx’s monorepo.tools defines a monorepo as one repository with multiple distinct projects and well-defined relationships—not mere “code colocation” and not a single undivided giant app. That framing matters in 2026 because repo boundaries are walls for AI agents as much as for humans: polyrepos force you to re-explain APIs and types at every edge; monorepos let agents read implementations directly.
Citation is not deployment. Shipping apps/web to Vercel does not mean you ship externals/python-substack to the same host—you still build and deploy each artifact; the monorepo only guarantees they evolve together in Git.
Monorepo vs polyrepo
| Dimension | Monorepo | Polyrepo |
|---|---|---|
| Cross-package refactor | One PR updates API + all callers | Coordinate releases across repos |
| Shared code | packages/* imported via workspace protocol | Publish to npm/PyPI or duplicate |
| CI | One pipeline; affected-only with Nx/Turbo | Per-repo pipelines |
| Ownership | Needs CODEOWNERS / path rules | Repo boundary = team boundary |
| Clone size | Grows with everything | Smaller per repo |
| Access control | Finer-grained in Git hosting | Repo-level permissions |
Good monorepo fit: one product family, shared types, agents + web + MCP maintained by overlapping teams.
Good polyrepo fit: legally separate products, wildly different release cycles, or open-source libraries with external contributors who should not see internal apps.
Anatomy of a modern product monorepo
A layout common in Next.js + agents + MCP products (including directories like ExplainX that combine web apps, Prisma packages, and Python tooling):
my-product/
├── apps/
│ ├── web/ # Next.js marketing / app
│ ├── newsletter/ # Second Next.js or API app
│ └── learn/ # Another surface
├── packages/
│ ├── db/ # Prisma client + schema
│ ├── ui/ # Shared React components
│ └── config-eslint/ # Shared lint config
├── externals/
│ └── python-substack/ # Python lib + MCP server
├── .agents/
│ └── skills/ # Agent SKILL.md bundles
├── .claude/
│ └── skills/ # Mirror for Claude Code
├── chrome-apps/ # Extension (optional)
├── scripts/ # Repo-wide automation
├── package.json # JS workspace root
└── .github/workflows/ # CI
Rules of thumb:
apps/*— things users or customers run in production browsers/services.packages/*— libraries apps depend on; never importapps/webfrompackages/db.externals/*— Python, forks, or submodules that use their ownpyproject.toml..agents/skills— not npm packages; consumed by Cursor, Claude Code, or compatible harnesses (agent skills guide).
JavaScript and TypeScript monorepos
npm / pnpm / Yarn / Bun workspaces
All major JS package managers support workspaces: multiple package.json files linked locally.
Root package.json (Bun-style workspaces):
{
"name": "my-product",
"private": true,
"workspaces": ["apps/*", "packages/*"],
"scripts": {
"dev": "bun run --filter '@my-product/web' dev",
"build": "bun run --filter '*' build"
}
}
Consumer app (apps/web/package.json):
{
"name": "web",
"private": true,
"dependencies": {
"db": "workspace:*",
"ui": "workspace:*"
}
}
The workspace:* protocol resolves db to packages/db on disk—no publish step during development.
pnpm uses pnpm-workspace.yaml (required at the root—pnpm does not use only package.json workspaces):
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
# Optional: centralize dependency versions (pnpm 9+)
catalog:
react: "19.2.4"
next: "16.2.2"
typescript: "^5.8.0"
Then reference catalogs from member package.json files with "react": "catalog:" to avoid version drift across apps.
Filter scripts without Turbo: pnpm --filter web dev runs one package; pnpm -r build runs build in all workspace members in dependency order.
References: npm workspaces, pnpm workspaces, pnpm catalogs, Bun workspaces.
Shared package example (packages/db)
{
"name": "db",
"version": "0.0.0",
"type": "module",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"db:generate": "prisma generate",
"postinstall": "prisma generate"
},
"dependencies": {
"@prisma/client": "^6.1.0",
"prisma": "^6.1.0"
}
}
// packages/db/src/index.ts
import { PrismaClient } from "../generated/client";
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
export const prisma =
globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== "production") {
globalForPrisma.prisma = prisma;
}
export * from "../generated/client";
// apps/web/app/api/skills/route.ts
import { prisma } from "db";
export async function GET() {
const skills = await prisma.skill.findMany({ take: 10 });
return Response.json(skills);
}
One schema change in packages/db → regenerate client → TypeScript errors in every app that imports db in the same PR. That is the monorepo superpower.
Turborepo and Nx (orchestration)
When build, test, and lint run across ten packages, naive npm run build --workspaces rebuilds everything every time.
Turborepo (vercel/turborepo) adds a task graph, local/remote caching, and content-addressed hashing so unchanged packages skip work. Public signals as of May 2026: roughly 30,000+ GitHub stars, homepage at turborepo.dev, active 2.9.x release line. Practitioner write-ups report large CI time reductions after migrating to Turborepo 2.x—treat as directional, not a guarantee on your graph.
Important: Turborepo 2.x renamed the top-level config key from pipeline to tasks. Older tutorials still show pipeline; that fails schema validation on 2.x. Migrate with:
npx @turbo/codemod migrate
{
"$schema": "https://turborepo.dev/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
},
"lint": {},
"test": { "dependsOn": ["^build"] }
}
}
The ^build microsyntax means “run build in dependencies first”—exactly what you want when apps/web imports packages/db.
Nx (nrwl/nx) adds affected detection, generators, and optional Nx Cloud CI distribution. As of May 2026: roughly 28,500 GitHub stars, ~9M weekly npm downloads on the core nx package (per npm registry stats), latest stable around 22.7.x. Nx’s own positioning in 2026 explicitly includes AI agents—caching and affected runs matter when agents trigger full-repo builds more often.
Open-source agent workflow repos such as gstack (covered in our gstack deep dive) use Nx-style layouts for slash commands and shared tooling.
When to add orchestration: CI exceeds ~10 minutes, or developers routinely skip building packages they did not touch (and break main). Many small teams stay on pnpm -r alone until pain appears—that is a valid 2026 pattern (pnpm production monorepo notes).
Next.js in a monorepo
Next.js does not require a monorepo, but monorepos are the default for teams running multiple Next apps (marketing + logged-in product) plus shared UI.
Minimal Next app package
{
"name": "web",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "16.2.2",
"react": "19.2.4",
"react-dom": "19.2.4",
"db": "workspace:*"
}
}
transpilePackages for workspace libraries
Next.js can compile dependencies from local packages or node_modules via transpilePackages (stable since v13.0.0 per Next.js docs).
You strictly need it when:
- The workspace package is resolved as a symlinked
node_modulesboundary and Next would otherwise skip transpilation - The package imports CSS or assets that trigger “Global CSS cannot be imported from within node_modules”
- You export raw TS/TSX without a separate build step and the bundler treats the package as external
You may not need it when the bundler resolves the package like local source (common in some Turborepo/Nx setups with project references)—a Next.js discussion (#93542) documents both behaviors.
// apps/web/next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ["ui", "db"],
};
export default nextConfig;
Turbopack note (2026): monorepo + subpath exports in internal packages can behave differently under Turbopack vs Webpack; if internal packages fail to resolve, check Next.js issue #85315 and fall back to --webpack until your graph is supported.
Environment variables
Each app keeps its own .env.local. Database URLs usually live in apps/web and apps/learn, while packages/db reads process.env.DATABASE_URL at runtime—document which app owns which secret in README or AGENTS.md so coding agents do not guess wrong.
Anti-pattern: importing upward
// ❌ packages/ui importing from apps/web
import { Header } from "../../apps/web/components/Header";
// ✅ move shared component into packages/ui
import { Header } from "ui";
Dependency direction should always flow apps → packages, never the reverse.
Python in a monorepo
Python does not use npm workspaces. Common patterns:
| Pattern | Tools | Use when |
|---|---|---|
Per-service pyproject.toml | Poetry, setuptools | One MCP server or API per folder |
| uv workspace | uv | Multiple Python packages, one lockfile |
| Requirements + venv | pip | Scripts only; weakest for multi-package |
Poetry package with optional MCP extras
Real-world MCP servers often declare MCP dependencies as an optional group so library users are not forced to install FastMCP:
# externals/python-substack/pyproject.toml
[tool.poetry]
name = "python-substack"
version = "0.1.22"
packages = [{ include = "substack" }]
[tool.poetry.dependencies]
python = "^3.10"
requests = "^2.32.0"
python-dotenv = "^1.2.1"
[tool.poetry.group.mcp]
optional = true
[tool.poetry.group.mcp.dependencies]
fastmcp = "^3.1.1"
Install for MCP development:
cd externals/python-substack
poetry install --with mcp
Minimal FastMCP server in the same tree
# externals/python-substack/substack_mcp/mcp_server.py
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("substack")
@mcp.tool()
async def post_draft_from_markdown(title: str, body_markdown: str) -> dict:
"""Create a draft post from markdown."""
# call into substack.api ...
return {"status": "draft_created", "title": title}
if __name__ == "__main__":
mcp.run()
Reference: Model Context Protocol Python SDK, FastMCP.
uv workspace (multi-package Python)
For several Python libraries in one repo:
# pyproject.toml (repo root)
[tool.uv.workspace]
members = ["packages/py-common", "services/ingest", "services/mcp-bridge"]
# services/mcp-bridge/pyproject.toml
[project]
name = "mcp-bridge"
dependencies = ["py-common", "mcp>=1.0"]
[tool.uv.sources]
py-common = { workspace = true }
Reference: uv workspaces.
Caveats from official uv docs: workspaces share one uv.lock and an intersected requires-python across members—if one service needs Python 3.14 and another caps at 3.11, use path dependencies with separate envs instead of one workspace. Large open-source examples (e.g. Apache Airflow’s multi-package tree, discussed on Talk Python #540) use uv workspaces for per-package uv sync so import boundaries match declared dependencies.
CI: two languages, one pipeline
# .github/workflows/ci.yml (illustrative)
jobs:
js:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun run build
- run: bun test
python-mcp:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- run: uv sync --directory services/mcp-bridge
- run: uv run pytest services/mcp-bridge
Path filters (paths: ['apps/**', 'packages/**']) avoid running Python jobs on doc-only commits.
AI agents in a monorepo
Agent skills are not npm packages. They are markdown instruction bundles (often SKILL.md + references) that Claude Code, Cursor, or compatible harnesses load when a task matches.
Typical layout:
.agents/skills/
├── blog-writing/
│ ├── SKILL.md
│ └── templates/
├── seo-geo/
│ └── SKILL.md
└── frontend-design/
└── SKILL.md
Why monorepo + skills:
- Same repo the agent edits gets the same conventions (blog frontmatter, Prisma patterns, MCP import scripts).
- One PR can update a skill and the code it describes.
- Discovery:
npx skills addand registries (ExplainX skills) distribute skills; in-repo copies stay pinned to your branch.
Example skill frontmatter agents read:
---
name: blog-writing
description: Write SEO/GEO-optimized blog posts for explainx.ai following established patterns.
---
# Blog Writing Skill
Content location: `/apps/web/content/blog/`
Agent harness note: Terminal agents (OpenClaw, Claude Code, etc.) benefit from monorepos because search and grep span the whole product—but large repos need .cursorignore / context rules so agents do not load node_modules or generated Prisma clients into context. Tools like incremental indexes (CocoIndex) target exactly this “living monorepo” problem.
2026 industry framing: monorepo.tools and Nx’s “polyrepo tax” narrative emphasize atomic cross-project PRs, single-source shared libraries, and agent-visible context across frontend, backend, and tools—without claiming every company should merge all repos tomorrow. Synthetic monorepos (dependency graphs across separate Git repos) are an incremental option when full consolidation is blocked.
MCP servers in a monorepo
Model Context Protocol (MCP) standardizes how hosts (Cursor, Claude Desktop, IDE extensions) call tools and read resources from external processes.
MCP servers in a monorepo are usually:
- TypeScript — official SDK in
packages/mcp-fooortools/mcp-foo - Python — FastMCP or the Python SDK in
externals/*orservices/* - Not embedded in Next.js — they run as stdio/SSE processes configured in the IDE
TypeScript MCP SDK versions (mid-2026)
The modelcontextprotocol/typescript-sdk repo (~12,500+ stars as of May 2026) documents two generations:
| Track | Packages | Status |
|---|---|---|
v1.x (v1.x branch) | @modelcontextprotocol/sdk + zod peer | Recommended for production; latest stable around v1.29.0 (Mar 2026) |
v2 (main branch) | Split: @modelcontextprotocol/core, client, server; Node 20+, ESM-only | Pre-stable; migration guide |
Use v1 in monorepo MCP packages until v2 GA unless you are explicitly experimenting.
TypeScript MCP server package sketch (v1)
{
"name": "@my-product/mcp-github",
"type": "module",
"bin": { "mcp-github": "./dist/index.js" },
"dependencies": {
"@modelcontextprotocol/sdk": "^1.29.0",
"zod": "^3.25.0"
}
}
// packages/mcp-github/src/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({ name: "github", version: "1.0.0" });
server.tool(
"list_prs",
{ repo: z.string().describe("owner/name") },
async ({ repo }) => {
const prs = await fetchPRs(repo);
return { content: [{ type: "text", text: JSON.stringify(prs, null, 2) }] };
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
Install per official README: npm install @modelcontextprotocol/sdk zod.
Cursor / Claude config at repo root
{
"mcpServers": {
"github": {
"command": "bun",
"args": ["run", "--filter", "@my-product/mcp-github", "start"]
},
"substack": {
"command": "poetry",
"args": ["run", "python", "-m", "substack_mcp.mcp_server"],
"cwd": "${workspaceFolder}/externals/python-substack"
}
}
}
Separation of concerns:
| Layer | Role |
|---|---|
Next.js apps/web | Human UI, public APIs, SEO |
| Agent skills | How the model should write/review code |
| MCP servers | Live tools (DB, GitHub, Substack, browsers) |
packages/db | Shared data access for apps—not a substitute for MCP unless exposed via a server |
Browse real server patterns in the ExplainX MCP directory and introducing MCP on ExplainX.
Versioning and publishing internal packages
Not every package in a monorepo ships to npm. Apps stay private: true. Libraries you publish need explicit versioning.
@changesets/cli is the common JS flow:
- Developer runs
changesetand describes bump (patch/minor/major). - Changesets accumulate under
.changeset/. - On release, CI versions packages and updates changelogs.
# One-time setup at repo root
bun add -D @changesets/cli
bun changeset init
<!-- .changeset/brave-lions.md -->
---
"db": patch
---
Fix connection pool defaults for serverless builds.
Lerna and Beachball solve similar problems; greenfield repos in 2026 usually pick Changesets + pnpm or Changesets + Turborepo.
Python: publish packages/py-common to PyPI independently with uv publish or Poetry poetry publish; tag releases in Git matching the monorepo tag py-common-v0.4.0 if multiple artifacts leave one repo.
Deploying from a monorepo
Each app is still one deployable unit. Platforms read a root directory or filter:
| Platform | Pattern |
|---|---|
| Vercel | Project root = apps/web; install from repo root with cd ../.. && bun install |
| Docker | Multi-stage build: copy root lockfile → install → bun run --filter web build |
| Fly.io / Railway | dockerfile per app or monorepo-aware buildpack |
Illustrative Dockerfile (Next app in workspace):
FROM oven/bun:1 AS deps
WORKDIR /app
COPY package.json bun.lock ./
COPY apps/web/package.json apps/web/
COPY packages/db/package.json packages/db/
RUN bun install --frozen-lockfile
FROM deps AS builder
COPY . .
RUN bun run --filter web build
FROM node:22-slim AS runner
WORKDIR /app
COPY --from=builder /app/apps/web/.next/standalone ./
ENV NODE_ENV=production
CMD ["node", "server.js"]
MCP and Python services deploy as separate containers or run on developer machines—do not co-locate with Next unless you operate a process supervisor (usually unnecessary).
Reference: Vercel monorepo docs.
Security and boundaries in agent-heavy monorepos
Monorepos plus agents + MCP increase blast radius if secrets leak into model context.
| Practice | Why |
|---|---|
Per-app .env.local | Limits what a compromised apps/web route exposes |
.cursorignore / .gitignore for .env* | Stops agents indexing secrets |
| Scoped MCP tools | GitHub MCP token with read-only PR scope, not org admin |
CODEOWNERS on packages/db/prisma | Schema changes need data-team review |
| No production DB URLs in skills | Skills are markdown—treat as public |
MCP security and agent skill verification apply unchanged; monorepos concentrate assets, so access control moves to path-based review.
Putting it together: one feature across the stack
Goal: Add “export skills to CSV” — touches DB schema, web API, docs, agent skill, optional MCP tool.
| Step | Location | Action |
|---|---|---|
| 1 | packages/db/prisma/schema.prisma | Add SkillExport model |
| 2 | packages/db | prisma migrate dev + commit generated client |
| 3 | apps/web/app/api/... | Route using prisma from db |
| 4 | apps/web/content/blog/... | Document feature (BLUF + FAQ) |
| 5 | .agents/skills/... | Update skill: “run export via API route X” |
| 6 | packages/mcp-skills (optional) | MCP tool wrapping same API for Claude |
One pull request, one CI run, reviewers see the full vertical slice—impossible to coordinate that cleanly across five polyrepos without release choreography.
Tooling cheat sheet
| Tool | Ecosystem | Primary benefit |
|---|---|---|
| npm/pnpm/Yarn/Bun workspaces | JS/TS | Link local packages |
| Turborepo | JS/TS | Cache + task pipeline |
| Nx | JS/TS (+ plugins) | Affected graph, generators |
| Poetry | Python | Lockfile + optional dependency groups |
| uv | Python | Fast installs + workspaces |
| Bazel / Pants | Polyglot | Hermetic builds at huge scale |
| Changesets / Beachball | JS | Versioning + changelogs per package |
| Lerna (legacy) | JS | Publishing; less common for app monorepos |
Ecosystem scale (verified May–June 2026):
| Project | Signal |
|---|---|
| vercel/turborepo | ~30,100 GitHub stars; docs at turborepo.dev |
| nrwl/nx | ~28,500 GitHub stars; ~9.1M/week npm downloads on nx |
| modelcontextprotocol/typescript-sdk | ~12,500 GitHub stars; v1.x for production MCP servers |
| monorepo.tools | Curated monorepo education (Nx-maintained) |
Choose orchestration when pain appears, not on day one.
Common mistakes
| Mistake | Fix |
|---|---|
| Circular deps between packages | Enforce apps → packages only; extract shared module |
Checking in node_modules | Root lockfile + CI cache |
One global .env for all apps | Per-app env files + documented ownership |
| Bundling MCP into Next server | Run MCP as separate process; use API if needed from web |
| Agents read whole repo | Ignore generated output, .next, node_modules |
| Python + JS same package name | Namespace: @org/web vs org_py_common |
| No affected CI | Path filters or Turbo/Nx affected |
When to start (and when to split)
Start a monorepo when:
- You have 2+ deployables sharing types, auth, or design system
- You ship agents + product and want skills aligned with code
- You maintain MCP tools beside the app that consumes their data
Split or stay polyrepo when:
- Teams need independent version lines with no coordination
- Open-source boundary must not leak private apps
- Repo clone / CI cost exceeds team capacity to invest in caching
Related reading
- What is MCP? — host, client, server architecture
- What are agent skills? — SKILL.md and harnesses
- gstack: Claude Code skills factory — opinionated monorepo + process
- Microsoft APM and workspace agents — monorepo agent dependencies
- RAG vs MCP — retrieval vs tool protocol
- Browse MCP servers · Browse agent skills
External references: Google monorepo engineering (overview) · monorepo.tools · pnpm · Turborepo · Nx · uv · MCP specification · MCP TypeScript SDK v1.x
Summary
A monorepo is one Git repository holding many related packages—Next.js apps, shared TypeScript libraries, Python MCP servers, and agent skills—so you can refactor across boundaries in a single change. JavaScript workspaces link apps/* to packages/*; Python typically uses separate pyproject.toml files or uv workspaces under externals/ or services/. Next.js consumes shared packages via workspace:* and transpilePackages. Agents read repo-level skills; MCP runs as separate stdio servers configured in the IDE, not inside the Next bundle.
Pick workspaces first, add Turborepo or Nx when CI hurts, and keep dependency arrows pointing inward toward shared packages. That layout is how modern AI-native product teams ship web, data, tools, and coding agents from one place.
Tool versions (Next.js 16.x, Prisma 6.x, FastMCP 3.x, MCP SDK v1.29.x, Turborepo 2.9.x, Nx 22.7.x) and star/download figures were checked against upstream docs and registries in May–June 2026; re-verify before production upgrades.