afterbuild/ops
ERR-888/Vercel · Build
ERR-888
Module not found — or — process.env.X is undefined at build time

appears when:When the local environment differs from Vercel builders in Node version, filesystem case sensitivity, or build-time env vars

Build succeeds locally but fails on Vercel

Your Mac is case-insensitive. Vercel's Linux builders are not. Your Node is 22. Vercel might be 18. Your .env.local is not in Project Settings. Any one breaks the build.

Last updated 17 April 2026 · 7 min read · By Hyder Shah
Direct answer
Local and Vercel environments differ in three observable ways: Node version, filesystem case sensitivity, and env variables. Pin Node with engines.node in package.json, grep your imports for any mismatch against disk casing, and confirm every process.env.FOO referenced at build time is set in Vercel Project Settings under the correct environment (production, preview).

Quick fix for build succeeds locally but fails on Vercel

package.json
json
01// package.json — pin Node version and guard casing02{03  "engines": {04    "node": "20.x"05  },06  "scripts": {07    "build": "next build",08    "build:ci": "CI=true NODE_ENV=production next build",09    "check:case": "next lint --strict"10  }11}12 13// Reproduce the Vercel environment locally before pushing:14//   nvm use 2015//   rm -rf node_modules .next16//   npm ci17//   NODE_ENV=production npm run build18 19// On macOS, create a case-sensitive volume for testing:20//   diskutil apfs addVolume disk1 "Case-sensitive APFS" ci-build21//   cd /Volumes/ci-build && git clone <repo>22//   run build here — catches every casing bug before deploy
Pin Node, add a CI-mode build script, and run production builds locally on a case-sensitive volume

Deeper fixes when the quick fix fails

01 · GitHub Actions build that matches Vercel

.github/workflows/build.yml
yaml
01# .github/workflows/build.yml02name: Build (matches Vercel)03on: pull_request04jobs:05  build:06    runs-on: ubuntu-latest # case-sensitive like Vercel07    steps:08      - uses: actions/checkout@v409      - uses: actions/setup-node@v410        with:11          node-version: 20 # match Vercel Project Settings12      - run: npm ci13      - run: npm run build14        env:15          # set the same build-time vars Vercel has16          DATABASE_URL: ${{ secrets.DATABASE_URL }}17          NEXT_PUBLIC_API_URL: ${{ vars.NEXT_PUBLIC_API_URL }}
CI on Linux with pinned Node catches casing and Node-version bugs before Vercel sees them

02 · Safe env access at module scope

lib/env.ts
typescript
01// lib/env.ts — validated env access, single source of truth02import { z } from "zod";03 04const schema = z.object({05  DATABASE_URL: z.string().min(1),06  NEXT_PUBLIC_API_URL: z.string().url(),07  STRIPE_SECRET_KEY: z.string().startsWith("sk_"),08});09 10const parsed = schema.safeParse(process.env);11 12if (!parsed.success) {13  console.error("Invalid environment variables:");14  console.error(parsed.error.flatten().fieldErrors);15  throw new Error("Missing required environment variables at build time");16}17 18export const env = parsed.data;
Fail fast with readable errors at build time — no more undefined references deep in the stack

03 · Check imports against disk casing in CI

scripts/check-casing.mjs
javascript
01// scripts/check-casing.mjs — fails CI if any import mismatches disk02import { execSync } from "node:child_process";03import { existsSync, readFileSync } from "node:fs";04import { dirname, resolve } from "node:path";05 06const files = execSync("git ls-files '*.ts' '*.tsx'").toString().split("\n").filter(Boolean);07let failed = 0;08 09for (const file of files) {10  const content = readFileSync(file, "utf-8");11  const imports = content.matchAll(/from\s+["'](\.\.?\/[^"']+)["']/g);12  for (const [, path] of imports) {13    const full = resolve(dirname(file), path);14    const candidates = [full, full + ".ts", full + ".tsx", full + "/index.ts"];15    if (!candidates.some((c) => existsSync(c))) {16      console.error(`Casing mismatch: ${file} imports ${path}`);17      failed++;18    }19  }20}21 22process.exit(failed > 0 ? 1 : 0);
Run in CI on Linux — catches the casing class of bugs before they reach Vercel

Why AI-built apps hit build succeeds locally but fails on Vercel

The local-vs-Vercel gap is a classic "works on my machine" problem dressed up in modern tooling. Three variables diverge: the Node runtime, the filesystem rules, and the environment variables. Each is invisible until the build actually runs, and each produces an error message that does not name the real cause.

Node version drift is the subtlest. Mac developers use nvm or fnm and often end up on whatever the latest LTS is — today, 22. Vercel projects created before Fall 2024 default to Node 18, which lacks fetch native in some edge cases and has different fs/promises semantics. A single await using statement or a top-level import of node:fs/promises with named imports Node 18 does not support will crash Vercel's build while your Mac just shrugs. The fix is to pin with engines.node in package.json and verify Project Settings matches.

Filesystem case is the most frustrating because the failure message lies. You write import Logo from "./components/Logo" but the file on disk is logo.tsx. On macOS APFS with the default case-insensitive mode, those map to the same inode. Your local build succeeds. Linux ext4 on Vercel treats them as different paths, so the import resolves to a path that does not exist. Webpack reports Module not found: Can't resolve ./components/Logo. You spend an hour convinced the file is missing until you ls -la and see it is logo.tsx all along.

Env variables at build time are the most common cause for AI-scaffolded apps. A Lovable or Cursor scaffold references process.env.DATABASE_URL at module scope for Prisma client initialization — that runs during build. Locally, .env.local is loaded. On Vercel, that file is gitignored and never uploaded. If you forgot to set the variable in Project Settings, Prisma fails with undefined. The error says "invalid connection string" — no mention of the missing env var.

build succeeds locally but fails on Vercel by AI builder

How often each AI builder ships this error and the pattern that produces it.

AI builder × build succeeds locally but fails on Vercel
BuilderFrequencyPattern
LovableEvery first deployForgets to add .env.local keys to Vercel Project Settings
Bolt.newCommonUses top-level fetch (Node 18+) without pinning engines.node
CursorCommonSuggests imports with arbitrary casing; relies on macOS case-insensitivity
Base44SometimesDefaults to pnpm lockfile while Vercel uses npm — dependency resolution differs
Replit AgentRareAdds OS-specific native modules (e.g. fsevents) not installed in Linux

Related errors we fix

Stop build succeeds locally but fails on Vercel recurring in AI-built apps

Still stuck with build succeeds locally but fails on Vercel?

Emergency triage · $299 · 48h turnaround
We restore service and write the root-cause report.
start the triage →

build succeeds locally but fails on Vercel questions

Why does Vercel build fail when my local build passes?+
The three most common causes: Node version mismatch (Mac runs 20, Vercel default is 18 or 22 depending on project creation date), case-sensitive filename issue (macOS is case-insensitive by default, Vercel builders run Linux which is case-sensitive), and env variables referenced at build time that are set locally but not in Vercel project settings. The first two are silent on local; the third gives a cryptic undefined error at build step.
How do I pin the Node version on Vercel?+
Two options. First, set 'engines.node' in package.json to your exact version — Vercel respects it up to minor version. Second, go to Project Settings → General → Node.js Version and pick explicitly. The package.json method wins if both are set. Never use > or ^ ranges — pin to exact major.minor like '20.x' or '22.x'. Vercel deprecates old versions; check their compatibility page before pinning to 18.
Why does a case-sensitive import break only on Vercel?+
macOS uses HFS+ or APFS formatted case-insensitive by default. Writing import Button from './components/button' works locally because the filesystem treats button.tsx and Button.tsx as the same file. Vercel builds on Linux with case-sensitive ext4. The import fails because 'button' does not exist — only 'Button' does. The error message says 'module not found' which does not hint at the real cause. Fix by matching every import statement to the exact casing on disk.
What env variables does Vercel need at build time versus run time?+
Variables prefixed with NEXT_PUBLIC_ are inlined into the bundle during build — they must exist at build time. Server-only variables without that prefix are read at run time on the server. If you reference a non-public env var in getStaticProps, generateStaticParams, or at module top level, it becomes a build-time dependency. Set every variable in Vercel Project Settings → Environment Variables before first deploy, not after.
How long does a deployment audit take?+
For a single repo, reproducing the Vercel environment locally and fixing the failure takes 30-60 minutes. We pull the exact Node version, run NODE_ENV=production build on a case-sensitive filesystem, compare against Vercel logs, and patch. Our Deployment and Launch service is fixed-fee and includes a CI workflow that builds on Linux with the same Node version so local and CI catch issues before they hit Vercel.
Next step

Ship the fix. Keep the fix.

Emergency Triage restores service in 48 hours. Break the Fix Loop rebuilds CI so this error cannot ship again.

About the author

Hyder Shah leads Afterbuild Labs, shipping production rescues for apps built in Lovable, Bolt.new, Cursor, Replit, v0, and Base44. our rescue methodology.

Sources