Founder & Lead Engineer, RAITHub
When an app works locally but breaks in production, the cause is almost always a difference between the two environments, not your logic. Check in this order: environment variables, production build versus dev server, CORS, auth redirect URLs, serverless timeouts, Node version, case-sensitive paths, then the install command, database, file writes, cookies and third-party keys. The early ones are quickest to check.
This is common with apps built using Lovable, Bolt, Cursor or Claude Code, because the tool's preview or your laptop quietly supplies things production does not: a filled-in .env file, a forgiving dev server, localhost in every allow list. The examples use Next.js on Vercel with Supabase, but the causes apply to any stack and host. Each cause below gives the symptom, the check and the fix.
What are the 12 causes, at a glance?
| # | Cause | Typical symptom | Quickest check |
|---|---|---|---|
| 1 | Environment variables missing or different | "undefined" errors, blank data, 500s | Compare the host's variables with your .env |
| 2 | Production build differs from dev server | Build fails, or pages behave differently | Run the production build and start it locally |
| 3 | CORS | "blocked by CORS policy" in the browser console | Check the API's allowed origins |
| 4 | Auth redirect URLs | Login loops or lands on localhost | Check the auth provider's redirect allow list |
| 5 | Serverless timeouts | 504s or cut-off responses on slow calls | Check function duration in the host's logs |
| 6 | Node.js version | Syntax or API errors only on the server | Compare node -v locally and in the build log |
| 7 | Case-sensitive file paths | "Module not found" only after deploy | Compare import paths with real file names |
| 8 | Install command and dependency resolution | Build fails during install | Run the host's exact install on a clean clone |
| 9 | Database: migrations, pooling, wrong database | Missing columns, connection errors under load | Compare the schema in production with the code |
| 10 | Writing to the local file system | Uploads vanish or fail | Search the code for file writes |
| 11 | Cookies over HTTPS and domains | Logged in, then immediately logged out | Inspect the Set-Cookie header |
| 12 | Third-party keys, webhooks and allowed domains | Payments, email or maps fail only live | Check test versus live keys and webhook URLs |
1. Are your environment variables set, and set at the right time?
Missing or different environment variables are the first thing to rule out. Your .env file is, correctly, not committed, so production only has what you entered in the host's settings, for the right environment (production, preview or both).
Next.js adds a trap. Its environment variables guide says variables prefixed NEXT_PUBLIC_ are inlined into the browser bundle at build time, and "after being built, your app will no longer respond to changes to these environment variables". Change one on the host and you must redeploy. Non-prefixed variables are only available on the server, so browser code that reads them gets undefined. The docs also note that dynamic lookups such as process.env[name] are not inlined.
Make missing variables fail loudly at start-up instead of at the first request. A server-only module with Zod 4:
// src/lib/env.server.ts: import this from server code only
import { z } from 'zod'
const Env = z.object({
DATABASE_URL: z.url(),
SUPABASE_SERVICE_ROLE_KEY: z.string().min(1),
OPENAI_API_KEY: z.string().min(1),
NEXT_PUBLIC_SITE_URL: z.url(),
})
// Throws with the name of every missing or malformed variable.
export const env = Env.parse(process.env)
In browser code, keep referencing each public variable by its full name, process.env.NEXT_PUBLIC_SITE_URL, so Next.js can inline it. A wrong site URL shows up in odd places; RAITHub's own sitemap on the wrong domain is one example.
2. Have you run the production build, not just the dev server?
The dev server is forgiving on purpose. A production build type-checks, prerenders static pages, applies caching and bundles differently, so code that "works" in dev can fail to build or behave differently live. Run the same commands the host runs, locally:
npm run build
npm run start
Then click through the failing path on the local production server. Pages that read data at build time are a common surprise: a page prerendered once at build shows stale or empty data until it is rendered dynamically or revalidated. Framework conventions matter too; after Next.js 16 renamed middleware.ts to proxy.ts, a file with the old name simply stopped running, as described in proxy.ts middleware not running.
3. Is CORS blocking the browser?
If the browser console says a request was "blocked by CORS policy", your API does not list the production origin. CORS, cross-origin resource sharing, is in MDN's words an HTTP-header based mechanism that allows a server to indicate any origins other than its own from which a browser may load resources. Locally, the allow list often contains only http://localhost:3000.
Add the exact production origin, scheme included, and any preview domains you use. If requests send cookies, MDN is explicit: the server must return a specific origin, not the * wildcard, or the browser blocks the response. Fix CORS on the server; a browser extension that disables it only hides the problem on your own machine.
4. Do the auth redirect URLs include production?
If login loops, errors after the provider screen, or drops users back on localhost, the redirect URLs are wrong. On Supabase, the redirect URLs guide explains that the Site URL is the default redirect when none is given, and that the URL in redirectTo should match the Redirect URLs allow list. For Vercel, it suggests entries such as http://localhost:3000/** and https://*-<team-or-account-slug>.vercel.app/** for previews.
Set the Site URL to production, add production and preview URLs to the allow list, and check the OAuth provider's own callback settings as well, such as the authorised redirect URIs in Google or GitHub. Then sign in on production in a private window.
5. Is a serverless function timing out?
Long work that finishes on your laptop can be cut off on a serverless host, which ends any function that exceeds its maximum duration. AI calls, large imports and slow third-party APIs are the usual causes. Vercel's duration docs, with fluid compute enabled by default, list a default of 300 seconds on every plan and a maximum of 300 seconds on Hobby and 800 seconds on Pro and Enterprise. Other hosts and older configurations can be much shorter.
Check the function's duration in the host's logs. You can raise the limit per route in the Next.js App Router:
// app/api/generate/route.ts
export const maxDuration = 120 // seconds; must be within your plan's maximum
Raising the limit treats the symptom. For AI features, stream the response so the user sees progress, and move long jobs to a queue or background worker. Timeouts often hide a cost problem too; see why a Next.js app is slow in production.
6. Is production running a different Node.js version?
A newer syntax or API that works on your Node.js version can fail on an older one, and the reverse happens after upgrades. Vercel's Node.js versions page lists 24.x (the default), 22.x and 20.x, and lets engines in package.json override the project setting. Its changelog lists Node.js 20 as being deprecated on 1 October 2026.
{
"engines": { "node": "24.x" }
}
Pin the version in package.json, use the same major version locally, and confirm with node -v in the build log.
7. Do your import paths match the file names exactly?
Windows and macOS file systems are usually case-insensitive, and Linux build servers are case-sensitive. An import of ./components/Header finds header.tsx on your laptop and fails on the server with "Module not found".
Fix the import or the file name so they match exactly. Renaming only the case of a file can be missed by Git on case-insensitive systems; git mv header.tsx Header.tsx records it properly. Building in CI on Linux catches this before deploy.
8. Does production install dependencies the same way you do?
If the build fails during install, the host is resolving dependencies differently from your machine. This happened to RAITHub's own website on 27 September 2026: after a security upgrade moved nodemailer to v10, every local check passed and the Vercel build failed with npm ERESOLVE, a peer-dependency conflict with next-auth 4. Local checks ran npm ci, which installs the lockfile as-is; Vercel ran npm install, which re-resolves the tree and re-checks peer ranges. The fix was a scoped npm overrides entry, proved with a clean install, npm ls and npm audit. The full write-up is fixing npm ERESOLVE on Vercel.
The rule it taught: run the host's exact install and build commands on a clean clone before you blame the code.
9. Is production using the database you think it is?
Three database differences cause "works locally" bugs. First, migrations applied locally but not to production, so a column the code expects is missing; see Lovable and Supabase migrations in production. Second, connection limits: serverless functions open many short-lived connections, so use the provider's pooled connection string. Third, production pointing at a development database, or the reverse. Check which database your production environment variables name, and compare its schema with the code.
10. Does the code write files to local disk?
Serverless functions do not keep files written at run time between invocations, and deploy bundles are not the place for user data. Uploads saved to a local folder work on your laptop and disappear, or fail, in production. Store files in object storage, such as Supabase Storage or S3, and keep only temporary scratch files on local disk.
11. Are cookies being dropped over HTTPS or across domains?
If users log in and are immediately logged out, inspect the Set-Cookie header in the browser's network tab. Cookies marked Secure need HTTPS; a cookie set for the wrong domain is not sent back; and a front end and API on different domains need the right SameSite setting, plus the CORS credentials rules from cause 3. Serving the app and API from the same domain removes most of these problems.
12. Are third-party keys, webhooks and domains set for production?
Payment, email, maps and AI providers often behave differently live. Common gaps: test keys in production, or live keys missing; webhooks still pointing at a tunnel or localhost; API keys restricted to localhost referrers; an email domain not verified; a provider rate limit reached for the first time under real traffic. Check each provider's dashboard for the production URL, key mode and delivery logs. If a key was ever committed or exposed in the browser, rotate it; see Lovable exposing API keys.
How do you stop "works locally" bugs coming back?
- Validate environment variables at start-up with a schema, as in cause 1.
- Build in CI on Linux with the host's install command, the pinned Node.js version and a production build.
- Keep schema changes as migrations and apply them in the deploy pipeline.
- Use preview deployments with their own auth redirects and keys, and test there before production.
- Watch the logs after every deploy for the first errors, not the first complaints.
For an app built with an AI tool, these checks belong in a wider hardening pass; see how to make a vibe-coded app production-ready and the 30-day prototype-to-production plan.
Why RAITHub for this?
Because RAITHub reproduces the failure in the deploy environment before changing anything, and leaves a test or a check behind. The ERESOLVE fix above is RAITHub's own, published with versions, dates and the commands that proved it. This website carries 400+ tests, and RAITHub builds and fixes Next.js, TypeScript and PostgreSQL systems daily. A single broken deploy is a small fixed-scope job; start from the fix one issue page, or see the code rescue service if the deploy is one symptom of many.
When don't you need RAITHub?
- The table above finds it. Most of these causes take minutes to confirm and fix yourself.
- It is a platform setting on a hosted builder you cannot change. The platform's support team is the faster route.
- You need someone in the system within the hour. RAITHub starts with an audit call and a written quote, so it is not an on-call service.
Last reviewed: 29 September 2026. Vercel and Next.js documentation checked that day. The ERESOLVE incident was reproduced with npm 11.12.1 and Node.js 24.15.0.
If you have been through all 12 and production still breaks, send RAITHub the error as a "Fix a specific issue" request. Include the build or runtime log; the free 15-minute audit confirms the cause and you get a fixed quote for the fix.
Frequently asked questions
Why does my app work locally but not in production?
Because production differs from your machine: missing environment variables, a production build instead of the dev server, allow lists that only include localhost, shorter time limits, a different Node.js version or a case-sensitive file system. Check those in order before debugging your logic.
Why are my environment variables undefined after deploy?
Either they are not set on the host for that environment, or browser code is reading a server-only variable. In Next.js, NEXT_PUBLIC_ variables are inlined at build time, so redeploy after changing them, and reference each one by its full name.
Why does Supabase login redirect to localhost in production?
The Site URL or Redirect URLs in Supabase still point at localhost. Set the Site URL to production, add production and preview URLs to the allow list, and update the OAuth provider's callback settings.
Why does my build pass locally but fail on Vercel?
Often the install command differs. RAITHub's site passed with npm ci locally, but Vercel's npm install re-resolved dependencies and failed with ERESOLVE. Run the host's exact install and build commands on a clean clone to reproduce it.
How long can a Vercel function run?
With fluid compute, Vercel's docs list a 300-second default on every plan, a 300-second maximum on Hobby and 800 seconds on Pro and Enterprise, with longer durations in beta. Set maxDuration per route, and stream or queue long AI work.
Why do imports work on my Mac but fail on the server?
macOS and Windows usually ignore letter case in file names; Linux build servers do not. Make every import match the file name exactly, and use git mv to record case-only renames.
Related posts
Ready to discuss your project?
Book a free 15-minute technical audit with our engineering team.