A Next.js monorepo on Vercel is a genuinely good setup: one repo, shared packages, incremental builds, previews on every push. It is also a setup where a handful of specific problems will find you, and every one of them passes locally before failing in a deploy. These are the ones that actually cost me time, written down so they cost you less.
The shape
The layout is standard: an apps/web Next.js app, plus packages/* for shared code — in my case a database package that owns the Prisma schema and client. pnpm workspaces link it all together and Turborepo runs the build graph. Nothing exotic, and that is the point: this is the common shape, and the traps below are common with it.
The Prisma engine trap
This is the big one. Prisma ships a native query-engine binary, and on Vercel the bundler traces your imports to decide which files to include in the serverless function. When you import PrismaClient through a cross-package re-export in a pnpm monorepo — the natural thing, import { prisma } from '@acme/database' — the tracer follows the indirection but fails to pull the engine binary along with it. Locally everything works because the binary is right there in node_modules. In production your API routes return 503 with this:
the error you will actually see
PrismaClientInitializationError: Query engine library for current
platform "rhel-openssl-3.0.x" could not be located.
# 503 Service temporarily unavailable, only on Vercel, only at runtime
The fix has two parts. Import PrismaClient directly from @prisma/client inside the web app — and make @prisma/client a direct dependency of the app, not just of the database package — so the tracer sees a first-party dependency instead of a cross-package hop. And declare the deploy target in the schema so the right binary is generated:
schema.prisma (generator)
generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "rhel-openssl-3.0.x"]
}
Type-only imports bite too
Here is the sequel that cost me a second deploy. After fixing the runtime import, I still had import type { Prisma } from '@acme/database' in a couple of files — just types, erased at compile time, surely harmless. It type-checked locally and failed the Vercel build with Namespace has no exported member 'InputJsonValue'. On Vercel's pnpm layout, that re-export resolved to an un-generated .prisma/client stub. Import Prisma types from @prisma/client directly too. And leave the generator output at its default location — a custom output path had left the pnpm store's client un-generated on the build machine, which was the underlying cause.
The shared .next trap
This one is not about Vercel at all, but it will waste an afternoon locally. next build and next dev both write to apps/web/.next by default. If you run a production build while the dev server is still running, the build overwrites the dev server's compiled chunks underneath it, and the dev server starts throwing nonsense — random 500s, Cannot find module './447.js', routes that were fine a second ago.
Never runpnpm buildwhilenext devis up — they share.next. Stop the dev server first, or verify the production output withnext starton a spare port. If your dev server suddenly can't find modules it compiled fine a minute ago, this is almost always why.
Caching that actually helps
Turborepo's remote cache is the payoff for the monorepo structure, but only if outputs are declared correctly — Turbo has to know what a task produces to cache and restore it. For a Next.js build that means capturing .next while excluding its own cache directory.
turbo.json
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}
Get that right and a push that only touches the API skips rebuilding untouched packages entirely; get it wrong and Turbo either misses the cache every time or restores a stale build. It is worth the five minutes to check what each task actually emits.
The through-line
Every one of these has the same signature: it works on your machine and fails somewhere else, because the difference is in how a tool resolves files, not in your code. The habit that saves you is to treat "works locally" as necessary but not sufficient, and to make the real deploy environment — the Linux runtime, the pnpm store layout, the shared build directory — part of what you actually test. Write the fixes down in the repo's own notes the first time you hit them; the second time, you will be very glad they are there.
