You have a pnpm workspace with apps/web, apps/admin, and apps/docs. You push, and only apps/web shows up on the live domain. Admin and docs deployed silently to nothing or to the wrong subdomain. Or worse: Vercel shows three “Projects” but two of them never rebuild even when their source changes.
Fastest fix: open each Vercel project, go to Settings → Build and Deployment → Root Directory, and confirm each project points at its own subdirectory (apps/web, apps/admin, apps/docs). If two projects share the root (./) or point at the same subdir, they build the same app and only one domain wins. That single setting is the cause about 70% of the time. The other 30% is a Turborepo --filter that swallows the other apps, a Skip deployment toggle that never re-triggers a build, or a wrong output directory that deploys an empty shell.
This is the monorepo split-deploy trap. The fix is rarely in your code; it is in how each Vercel project is configured against the repo’s root directory, change-detection, and turbo filter scope.
Which bucket are you in?
Run the symptom against this table first, then jump to the matching cause below.
| Symptom | Most likely cause | Section |
|---|---|---|
| Two apps serve identical content; only one domain works | Shared/wrong Root Directory | Cause 1 |
| App’s source changed but Vercel says “skipped” / no deployment | Skip-deployment or Ignored Build Step too strict | Cause 2 |
Build log shows Tasks: 1 successful, 1 total instead of 3 | Turborepo --filter scope too narrow | Cause 3 |
pnpm ls -r does not list the missing app | Workspace glob excludes it | Cause 4 |
| Build “succeeds” but the URL serves a 404 page | Wrong Output Directory | Cause 5 |
| Pushing one app redirects another app’s domain | Duplicate project name / domain collision | Cause 6 |
| App deploys as an empty shell with no pages | App-level .vercelignore excludes its own pages | Cause 7 |
Common causes
Ordered by how often each catches teams.
1. Projects share the same Root Directory
Each Vercel project must point at its app’s subdirectory (apps/web, apps/admin, apps/docs). If two or more are set to repo root (./) or the same subdir, they all build the same default app. The duplicates produce the same output, and whichever project owns the live domain wins.
How to spot it: Open each Vercel project → Settings → Build and Deployment → Root Directory. If they are all blank or ./, this is it. (Vercel moved this setting out of the old General tab into Build and Deployment in the redesigned project settings; if your dashboard still shows it under General, the same field applies.)
2. The app never re-triggers a build
There are two separate change-detection mechanisms on Vercel, and they behave differently. Picking the wrong one (or misconfiguring it) is why an app whose source genuinely changed shows “skipped.”
- Skip unaffected projects (the modern default). As of June 2026 Vercel automatically skips a project in a monorepo unless its source, one of its internal dependencies, or a lockfile change that affects only its dependencies changed. This is enabled by default for GitHub-connected projects using npm / yarn / pnpm / Bun workspaces, and it does NOT consume a concurrent build slot. It only works if every workspace package has a unique
nameinpackage.jsonand cross-package dependencies are declared explicitly — ifapps/adminusespackages/uibut does not list it as a dependency, Vercel cannot see the link and skips admin whenuichanges. - Ignored Build Step (the manual escape hatch). A shell command you write (or
npx turbo-ignore). Per Vercel’s rule, exit0SKIPS the build and exit1or greater PROCEEDS. A canceled Ignored Build Step DOES count toward your concurrent-build and deployment limits, which is why the automatic skip is the better default for repos with many apps.
How to spot it: Push a change to apps/admin, watch Vercel. If you see “Build was skipped due to Ignored Build Step” but the apps/admin source did change, your Ignored Build Step path is wrong. If no deployment appears at all and you have no Ignored Build Step, the automatic skip likely could not see the change — usually a missing/duplicate package name or an undeclared internal dependency.
3. Turborepo filter scope mismatch
Build command is turbo run build --filter=web... from repo root. The web... filter only builds web and its dependencies. Admin and docs never get touched.
How to spot it: Build log shows Tasks: 1 successful, 1 total when you expected 3. Run turbo run build --dry (or --dry=json) and check the packages / task list — it confirms only one app is in scope.
4. Workspace glob excludes the missing app
packages: ["apps/web", "packages/*"] — admin/docs are not in the workspace, so install + build skip them entirely. They never become “real” packages, and Vercel’s automatic skip treats their changes as global (or misses them).
How to spot it: pnpm ls -r does not list admin/docs. pnpm install does not symlink them into node_modules.
5. Wrong output directory configured
For Next.js it is .next; for Astro dist; for Vite dist; for SvelteKit build. If the project’s output directory is misconfigured, Vercel finishes the build but uploads an empty or wrong directory, and the deploy “succeeds” with a 404 site.
How to spot it: Build log says success, deploy URL serves Vercel’s default 404 page. The Output Directory field in Settings → Build and Deployment is overridden to the wrong value.
6. Duplicate project name causes deploy collisions
If two Vercel projects share the same name, or you reuse a production domain across projects, the second one’s deploys overwrite the first’s domain aliases.
How to spot it: Two projects named frontend exist, or two projects claim the same domain. Pushing one redirects the other’s domain to the wrong app. Rename one and the symptom moves around.
7. App-level .vercelignore excludes critical files
A .vercelignore in apps/admin/ that excludes its own pages directory (typo or copy-pasted from another project) causes the app to deploy as an empty shell.
How to spot it: apps/admin/.vercelignore contains broad patterns like pages/ or app/. Cross-check against the app’s actual structure.
Before you start
Collect this for every deployable app — most fixes are obvious once these five facts are side by side:
- The deploy provider and the exact “Project” that maps to each app (
ls apps/to enumerate). - The Root Directory configured per project (from Settings → Build and Deployment).
- The Build Command and Output Directory per project.
- The package manager and workspace config (
pnpm-workspace.yaml, orpackage.jsonworkspaces). - The
turbo.jsoncontent, and the per-project Ignored Build Step command if one is set. - Domain aliases per project (Settings → Domains) and recent deploy logs showing what was built vs skipped.
Step-by-step fix
Ordered by ROI.
Step 1: Audit each project’s Root Directory
Vercel → for each project → Settings → Build and Deployment → Root Directory.
Expected:
apps/web → frontend project
apps/admin → admin project
apps/docs → docs project
If any is blank or ./, set it correctly and trigger a new deploy. This single setting is the most common cause. At import time the same field is set with the Edit button next to Root Directory before the first deploy.
Step 2: Let Vercel’s automatic skip do the work (then verify the graph)
For most teams, the right answer is to NOT write an Ignored Build Step at all and rely on the built-in skip. Confirm the prerequisites:
- The project is connected to GitHub and uses npm / yarn / pnpm / Bun workspaces.
- Every package in the workspace has a unique
namein itspackage.json. - Cross-package dependencies are declared, e.g.
apps/admin/package.jsonlists"@org/ui": "workspace:*"if it imports frompackages/ui.
The skip toggle lives at Settings → Build and Deployment → Root Directory → Skip deployment (set to Enabled to skip unchanged projects). Because it does not occupy a concurrent build slot, it scales better than the Ignored Build Step for many-app repos.
Step 3: Align the build command per project
Per project, the build command should resolve to only that app. On Vercel, with the Root Directory set correctly, the filter is auto-inferred and you can use:
turbo run build
If you need it explicit:
# apps/web project
cd ../.. && turbo run build --filter=@org/web
# apps/admin project
cd ../.. && turbo run build --filter=@org/admin
Or pnpm filtering:
pnpm --filter @org/web run build
The --filter value MUST match the package name in that app’s package.json, not the directory name. turbo is available globally on Vercel, so you do not have to add it as a dependency.
Step 4: If you DO use an Ignored Build Step, write it correctly
Reach for this only if the automatic skip cannot model your repo (non-JS workspace, custom branching rules). Remember the exit-code rule: exit 0 skips, exit 1+ builds.
# Skip deploy if no files in this app or its shared deps changed.
# git diff exits 1 (changes) -> build; exits 0 (no changes) -> skip.
git diff --quiet HEAD^ HEAD -- apps/admin packages/ui
The -- separates paths. Include the app directory AND any shared packages it depends on. If you only check the app dir, a change in packages/ui would not trigger a rebuild even though it should.
For Turborepo, prefer turbo-ignore, which walks the dependency graph:
npx turbo-ignore --fallback=HEAD^1
This is the exact command Vercel sets by default. The --fallback=HEAD^1 matters: on a brand-new branch there is no previously deployed SHA to compare against, so plain turbo-ignore builds everything to stay safe. --fallback=HEAD^1 tells it to compare against the parent commit instead, so new branches skip unaffected apps too.
Step 5: Ensure the Output Directory matches the framework
| Framework | Output |
|---|---|
| Next.js | .next (or auto-detected) |
| Astro | dist |
| Vite | dist |
| SvelteKit | build |
| Remix (Vite) | build/client |
In Settings → Build and Deployment, leave Output Directory empty if Vercel can auto-detect via the framework preset. Override only if your build writes elsewhere. If you use Turborepo Remote Caching, also confirm the outputs key in turbo.json matches the preset (e.g. [".next/**", "!.next/cache/**"] for Next.js), or a cache hit will report missing build outputs.
Step 6: Verify the workspace packages list includes every app
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
Or package.json:
{
"workspaces": ["apps/*", "packages/*"]
}
Globbing apps/* is safer than enumerating; new apps are picked up automatically. Anything outside the workspace definition is treated as a global change by Vercel’s skip logic and rebuilds every app, so keep deployable apps inside the glob.
Step 7: Test each project’s deploy independently
For each project:
- Make a trivial change in that app (touch a comment in a page).
- Push to the branch the project tracks.
- Verify a new deploy starts AND the live URL shows the change.
If a project does not redeploy, its change-detection (skip toggle or Ignored Build Step) is wrong. If it redeploys but the live URL does not update, the Output Directory or domain alias is wrong.
Step 8: Set explicit production domains per project
Vercel → each project → Settings → Domains:
apps/web → www.example.com
apps/admin → admin.example.com
apps/docs → docs.example.com
Each project should own one production domain. Sharing causes the collision in cause 6. Related: canonical domain change for handling alias swaps.
If apps need to reference each other across previews (frontend calling its own backend preview), use Vercel Related Projects: add { "relatedProjects": ["prj_..."] } to the app’s vercel.json (max 3 per app) and read the URLs from the VERCEL_RELATED_PROJECTS env var instead of hardcoding hosts.
How to confirm it’s fixed
- A trivial change in each app triggers a deploy for exactly that app’s project — and nothing else.
- Each app’s production URL serves that app’s content (not a 404 or the wrong app).
turbo run build --dryfrom the root lists the expected build graph per filter.- No project has Root Directory
./unless it is intentionally the only app. - Build logs show the expected Output Directory being uploaded (check the “Building” section of the deployment).
- Changing a shared package (
packages/ui) redeploys every app that depends on it, and only those.
Long-term prevention
- Document the project-to-app mapping in
apps/README.mdor a top-level CONTRIBUTING file. - Prefer Vercel’s automatic skip over hand-written diff commands; if you must script it, use
turbo-ignore --fallback=HEAD^1. - Give every package a unique
namewith a consistent@org/scope, and declare cross-package deps explicitly so the skip graph is accurate. - Add a CI step that fails if a new directory appears under
apps/without a matching deploy project. - Never set a Vercel project’s Root Directory to
./in a monorepo — it should always be a subdir. - When adding a new app, create the deploy project FIRST, then the code, so the project never lacks a deploy target.
Common pitfalls
- Setting an Ignored Build Step to
git diff --quiet HEAD^ HEADwithout a path — always exits 0, so deploys are SKIPPED forever (not “always build”, as people often assume — exit 0 means skip). - Using
--filter=webinstead of--filter=@org/webwhen the package is scoped — Turbo silently builds nothing. - Relying on the automatic skip while two packages share the same
name, or an internal dependency is undeclared — Vercel cannot build the graph and either over-builds or skips the wrong app. - Copy-pasting a project from Vercel’s “duplicate” feature and forgetting to change the Root Directory — both projects build the same app.
- Plain
npx turbo-ignoreon a brand-new branch builds every app because there is no SHA to diff against; add--fallback=HEAD^1. - Manually deploying with
vercel deployfrom the root and being surprised that only the root’s project receives it. See vercel build failed for adjacent CLI gotchas.
FAQ
Q: My app’s source changed but Vercel still skipped the deploy. Why?
Two usual reasons. If you have no Ignored Build Step, Vercel’s automatic skip could not link your change to the app — almost always a duplicate/missing package name or an undeclared internal dependency (e.g. apps/admin imports packages/ui but does not list it in package.json). If you do have an Ignored Build Step, the command exited 0 (which means skip): check the paths after the --.
Q: Do I even need an Ignored Build Step anymore?
For most JS monorepos on GitHub, no. The automatic “Skip unaffected projects” feature covers it, and unlike the Ignored Build Step it does not consume a concurrent build slot or count against your deploy limits. Reach for a manual Ignored Build Step (or turbo-ignore) only when the automatic skip cannot model your repo — non-JS workspaces, or custom branch-based rules.
Q: Can I deploy a whole monorepo to a single Vercel project?
Possible but not recommended. You would need routing/proxy config to map subroutes to subapps, and you lose per-app rollback and preview isolation. The standard answer is one Vercel project per deployable app.
Q: My monorepo uses Nx, not Turbo — does this still apply?
Same idea, different filter syntax. Use nx run web:build or nx affected --target=build --base=HEAD^ as the build command. The Root Directory and change-detection steps are identical; the automatic skip also works with plain npm/yarn/pnpm/Bun workspaces regardless of the task runner.
Q: Can I deploy a non-web package (a CLI, a Lambda)?
Not via Vercel — it expects a web framework. Deploy CLIs to npm or GitHub Releases and Lambdas to AWS or SST. See firebase deploy permission denied and vercel build failed for adjacent deploy-target mismatch issues.