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.
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
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 deployDeeper fixes when the quick fix fails
01 · GitHub Actions build that matches Vercel
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 }}02 · Safe env access at module scope
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;03 · Check imports against disk casing in CI
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);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.
| Builder | Frequency | Pattern |
|---|---|---|
| Lovable | Every first deploy | Forgets to add .env.local keys to Vercel Project Settings |
| Bolt.new | Common | Uses top-level fetch (Node 18+) without pinning engines.node |
| Cursor | Common | Suggests imports with arbitrary casing; relies on macOS case-insensitivity |
| Base44 | Sometimes | Defaults to pnpm lockfile while Vercel uses npm — dependency resolution differs |
| Replit Agent | Rare | Adds 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
- →Pin Node version in package.json engines.node to match Vercel Project Settings exactly.
- →Run every PR build in GitHub Actions on ubuntu-latest with the same Node version.
- →Use lowercase-kebab-case for all filenames to eliminate casing mismatches.
- →Validate env vars with Zod at module load so missing variables fail fast with a readable error.
- →Keep a documented .env.example in the repo listing every variable Vercel must have set.
Still stuck with build succeeds locally but fails on Vercel?
build succeeds locally but fails on Vercel questions
Why does Vercel build fail when my local build passes?+
How do I pin the Node version on Vercel?+
Why does a case-sensitive import break only on Vercel?+
What env variables does Vercel need at build time versus run time?+
How long does a deployment audit take?+
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.
Hyder Shah leads Afterbuild Labs, shipping production rescues for apps built in Lovable, Bolt.new, Cursor, Replit, v0, and Base44. our rescue methodology.