Factoring an AI-Native Codebase

There's a specific kind of codebase that exists in 2026 and didn't exist two years ago. It's a TypeScript monorepo — usually Next.js at the center — that started as a web app and gradually absorbed an AI agent layer, a vector database, a realtime multiplayer system, a 3D rendering pipeline, and three different worker runtimes. Nobody planned the architecture. The architecture is the sediment of every integration that shipped on a deadline.
I've been working inside one of these for months, and the experience has taught me more about software architecture than any greenfield project ever did — because the hard problems aren't the ones you plan for. They're the ones that emerge from the collision between systems that were never designed to share a process.

Drake's overstating it, but the core observation is sound. The typical AI-native codebase has a composition root — usually the Next.js app — that imports everything because everything needs to talk to everything. The agent layer calls the database. The database stores embeddings. The embeddings are generated by the AI client. The AI client needs auth from the web layer. The web layer renders 3D content that the agent can manipulate. Draw the dependency graph and it's a hairball.
The first instinct is to extract packages. You pull the AI client into @project/ai-clients. The content pipeline into @project/content-system. The asset handling into @project/asset-pipeline. The agent tools into their own module. This helps legibility — you can find things — but it doesn't help the hairball, because the packages still depend on each other in a cycle. You've organized the mess into labeled boxes. The mess is still a mess.
Therefore the real decomposition isn't about packages. It's about identifying the deployment boundaries — the places where you can draw a line and say "this side talks to that side over a wire, not through an import."

The deployment boundaries I keep finding in these codebases fall into a natural pattern. There are four distinct things pretending to be one application:
The web layer — pages, API routes, auth, payments, the UI. This is the product surface. It should be as thin as possible, and in practice it's the fattest layer because everything got routed through it.
The agent runtime — tool execution, thread persistence, memory compaction, model routing, observability. This is the part that noumen extracted into a standalone package. The insight there was that the agent loop is infrastructure, not product. Every AI app rebuilds the same state machine. Factor it out and the product layer gets to focus on what the agent should do, not how agents work.
The realtime layer — multiplayer state synchronization, presence, the game server. This naturally wants its own process because it has different scaling characteristics entirely. A web request is stateless and fast. A multiplayer session is stateful and long-lived. Running both in the same process means the long-lived connections starve the request handlers under load.
The worker layer — background tasks, messaging bots, gateway proxies, cron jobs. These are the things that run on timers or in response to external events, not user requests. They share domain logic with the web layer but have no business running in the same deployment.

Citrine's right, and the worker layer is where I see the most neglect in practice. But the decomposition isn't the hard part. The hard part is the contracts between the layers. Once the agent runtime lives in its own package, how does it access the database? Once the realtime server is its own process, how does it share type definitions with the web layer?
The answer I've landed on is aggressive schema generation. Zod schemas at the boundary. Generated TypeScript types from the database. OpenAPI specs for the API surface. The shared contract isn't code — it's types, generated from a single source of truth and consumed by every layer independently. When the database schema changes, the generated types change, and every layer that uses those types fails to compile until it's updated. The contract is enforced by the compiler, not by convention.

There's a subtler problem that schema generation doesn't solve: the build pipeline itself. In a large TypeScript monorepo with AI dependencies, the build is a minefield. Sharp wants native binaries. ONNX runtime ships WASM. Three.js plugins assume a browser environment. The LLM client streams over protocols that Turbopack doesn't understand yet. I've seen next.config.ts files with thirty externalized packages and comments explaining why each one can't be bundled.
The practical solution is to treat bundler policy as architecture. Which dependencies are never bundled defines the operational reality of your deployment. A package that requires native binaries can't run in a serverless function. A package that assumes window exists can't run in a worker. These aren't bugs — they're architectural constraints, and acknowledging them explicitly is better than fighting the bundler in every build.
The most counterintuitive decision I've made is separating typecheck from build. The TypeScript compiler runs out of memory on the full dependency graph before Next.js even starts bundling. Therefore the build runs with typescript.ignoreBuildErrors: true — yes, really — and a separate CI step runs tsgo --noEmit across the full workspace. The build verifies that the output works. The typecheck verifies that the code is correct. They're different questions answered by different tools, and pretending they're the same question is what makes the build take forty minutes and then OOM.

The deeper lesson of factoring an AI-native codebase is that the codebase isn't the product. The codebase is a map of the product's actual complexity, and when the map becomes illegible, the first instinct — redraw the map, reorganize the packages, add more abstraction — doesn't help. What helps is admitting that some parts of the system are different things that happen to share a repository, and giving them permission to be different. Different deployment targets. Different build strategies. Different scaling characteristics. Different owners, if the team is big enough.
The monorepo stays. The monolith dies. And the thing that replaces it isn't microservices — it's a small number of well-bounded deployables that share types, share a repo, and share nothing else.






