Monorepo
My default for a startup is to keep almost everything it builds in one repository: customer-facing apps, marketing surfaces, internal tools, the design system, database schemas, configuration and AI infrastructure.
Monorepos were already useful for humans. Coding agents make the tradeoff even more lopsided.
Context is infrastructure¶
Coding agents do better when the code, instructions, tools and feedback needed for a task are easy to find and operate.
A monorepo does not magically put the whole codebase into a model’s context window. It gives the agent a coherent source graph it can search and traverse. While working on a feature, it can look at the app, the shared components, the database schema, the internal tooling and every other consumer of the same package without crossing repo boundaries or resolving independently versioned codebases.
A monorepo is not just code organization. It makes more of the system legible to humans and agents.
This matters more as agents take on bigger changes. A schema change might also need updates to shared types, an API, the customer-facing product and an internal ops tool. When all of that lives in one graph, the complete change is easier to discover, make and review.
The inverse matters too. More code within reach is not automatically better context. As the repo grows, clear package boundaries, local docs and scoped agent instructions matter more and more.
One change, one PR¶
Cross-cutting changes are the strongest reason to do this.
A change to a shared contract updates the contract and every known consumer in the same branch and the same PR. Nobody has to publish an internal package, bump versions across five repos and babysit a sequence of merges while the system sits in multiple states.
Large refactors get easier for the same reason. Search the repo for the real dependency surface, change the interface and update its consumers together. The apps can still deploy independently. Git stays the unit of change.
Share the stable stuff¶
Share the primitives that should stay consistent across products: the design system, database schemas, types, validation, configuration, AI tooling and common application plumbing.
My stack for this is TypeScript everywhere, Next.js for the apps, the Vercel AI SDK for AI products, and pnpm workspaces with Turborepo to organize packages and coordinate tasks across the repo.
Not everything reusable belongs in a shared package. Shared abstractions create coupling too, and product-specific behavior should stay with the product that owns it. I am not after maximum reuse. I am after one canonical implementation of the things that need to stay consistent.
The repo is executable context¶
Source code is only part of what an agent gets from the repository.
It also gets instructions, dependency relationships, tests, build commands, lint rules, task definitions and dev tooling, i.e., ways to check its understanding against the actual system.
I use repo-level agent instructions and scope them further as apps and packages grow their own constraints. That turns architectural knowledge from something an agent has to infer into something the repo explicitly teaches.
One development surface¶
pnpm workspaces give you a single workspace and dependency graph for the TypeScript packages. Turborepo sits above that graph and coordinates builds, tests, linting, type checking and other tasks.
A monorepo does not mean every command has to run against the entire repository. Turborepo uses the dependency graph and task inputs to scope work to the affected parts of the repo, and caching lets unchanged work be reused locally and in CI. pnpm filtering is another way to target individual apps, packages or parts of the workspace.
So you keep most of the performance people reach for separate repos to get: selective builds and tests, parallel execution, isolated app development and independently deployable services, without giving up a shared source graph.
The goal is not to make the repository small. It is to make the amount of work triggered by any given change small.
Turbopack handles the Next.js bundling side and keeps the app development loop fast. It solves a different problem from Turborepo, but together they give you a consistent development surface across the Next.js apps.
The result is that an engineer or a coding agent can move between apps and keep the same package model, commands and conventions.
Branches become environments¶
This compounds with managed infrastructure. A branch can carry changes across an app, shared packages and the database schema, and get a live preview deployment plus an isolated fork of the production database. The change can be exercised as running software before it is merged.
It is also what lets PMs and designers work directly in the repo. They start a branch, use the real design system and data model, and work against running software instead of describing the change through mocks and specs. More on that in The branch is the PRD.
Sometimes engineering takes over the implementation. Sometimes they make a few changes. Sometimes they only need to review the PR. The shared repo makes all three possible.
Optimize for shipping¶
There are good reasons to split things up: hard source-code access boundaries, unrelated orgs, radically different dev environments or systems with almost no shared change surface.
A big monorepo also takes discipline. Dependency boundaries, ownership, task scoping, caching and agent instructions matter more as the graph grows.
For most startups those costs are worth paying. The products and internal systems operate on the same customers, data models, design language and AI infrastructure, and changes cross those lines all the time.
Keeping that system in one repository means humans and agents can reason about more of the real change at once, while modern workspace and build tooling keeps most individual operations scoped to the part of the graph that actually changed.
Optimize the repository for shipping.