Vibe-coded app works locally but breaks when deployed
"Works on my machine" means something differs in production. Check, in this order: which stage fails (build, browser or server), whether the production build fails on your machine too, environment variables (missing, or set but not redeployed), hardcoded localhost URLs, CORS, single-page-app routing, and file-name case or Node version differences. Most "it only breaks live" bugs are one of these.
What this guide covers
You built the app with Lovable, Bolt, Cursor, Claude Code, Replit or similar. It runs fine on your machine or in the builder's preview. Then you deploy it to your own host and something breaks: a blank page, a 500, a login that fails, data that won't load. The AI tool that wrote the code has no view of your production environment, so "ask it to fix it" tends to produce guesses. This is a short, repeatable way to find the actual difference instead.
Step 1: Work out which stage is failing
- The build fails. Open the build log on your host. The first error is usually the real one; the rest are fallout.
- The page loads but is blank, or something doesn't work. Open the browser's developer tools on the live site. The Console shows JavaScript errors and CORS messages, and the Network tab shows which request failed and with what status.
- A server route or API returns an error (500). Read your host's runtime logs. On Vercel, that's Logs in the project (Vercel: Runtime Logs), where you can search and inspect logs for a deployment.
Copy the exact error text. When you do ask an AI tool for help, give it the error and what you've already tried, one problem at a time, and commit your working version to git before each attempt so a bad fix is one command to undo.
Step 2: Reproduce the production build locally
The dev server and the production build are different programs. The production build bundles and minifies your code, and bakes some environment variables into it at build time (Vite's docs say its import.meta.env values are statically replaced at build time). Run what the host runs:
# install exactly what the lockfile says
npm ci
# build like the host does
npm run build
# then serve the build
npm run start # Next.js
npm run preview # Vite
If the problem appears here, you can debug it on your own machine. If it doesn't, the difference is in the environment: variables, URLs, versions or settings (Steps 3 to 7). Note that npm ci installs from the lockfile and fails if package.json and the lockfile disagree, which is itself useful to know before your host finds out.
Step 3: Environment variables (the most common cause)
- Make a list. Search the code for
process.envandimport.meta.env. Every name there needs a value in production. - Set them at the host, in the Production environment. Your
.envfile is meant to stay out of git (the defaultcreate-next-apptemplate adds.envfiles to.gitignore), so it never reaches the host. On Vercel you choose which environments (Production, Preview, Development) each variable applies to. - Redeploy. On Vercel, changes to environment variables apply only to new deployments, not existing ones (Vercel: environment variables).
- Know which ones get baked in. In Next.js, only
NEXT_PUBLIC_*variables reach the browser, and they're inlined at build time, so they're frozen to whatever the value was when you built. Non-prefixed variables are only available on the server (Next.js: environment variables). Vite exposes onlyVITE_*variables to client code (Vite: env variables and modes).
Don't "fix" a missing variable by adding the prefix to a secret. Anything named NEXT_PUBLIC_* or VITE_* is shipped to every visitor's browser. Vite's docs say outright that VITE_* variables should not contain sensitive information such as API keys. Keep secret keys on the server.
A missing server-side variable usually shows up as undefined in a 500 error in your runtime logs, or as a feature that silently does nothing.
Step 4: Hardcoded localhost URLs
Search your source for localhost, 127.0.0.1 and port numbers like :3000 or :5173:
grep -rn "localhost\|127\.0\.0\.1" src app lib components 2>/dev/null
In the browser, localhost means the visitor's own computer, not your server. A fetch('http://localhost:3000/api/...') that worked for you fails for everyone else. Use a relative path (/api/...) for calls to your own app, or a base URL read from an environment variable (Step 3). The same goes for OAuth redirect URLs, Stripe webhook URLs and email links; see our guides on Supabase auth redirects and Stripe webhooks.
Step 5: CORS errors
If the console says a request was blocked by a CORS policy, your front end and the API it calls are on different origins, and the API didn't say that your origin is allowed. CORS works through HTTP headers that let a server describe which origins may read its responses, and for requests that can change data the browser first sends a preflight OPTIONS request (MDN: CORS). Locally everything may have been same-origin, or the dev server may have proxied requests for you.
- The simplest fix is to call your own API at a relative path on the same domain, so there's no cross-origin request.
- If the API is separate, make it send
Access-Control-Allow-Originfor your production origin and answer theOPTIONSpreflight. MDN notes that for credentialed requests (cookies, HTTP auth) the server must name the origin, not use the*wildcard. - On Vercel, see How to enable CORS on Vercel.
Step 6: Refreshing a page gives 404 (single-page apps)
If deep links or a browser refresh on /dashboard return a 404 while clicking through the app works, you're probably serving a single-page app as static files. Vercel's Vite docs say deep linking won't work out of the box for a Vite SPA and show a vercel.json rewrite to index.html:
{
"rewrites": [
{ "source": "/(.*)", "destination": "/index.html" }
]
}
Other hosts have their own equivalent setting (a fallback to index.html), so look for "SPA fallback" or "rewrites" in your host's docs. Source: Vercel: Vite.
Step 7: Build differences that don't show up on your laptop
- File name case. macOS and Windows file systems are usually case-insensitive by default, so
import Header from './components/header'works locally even if the file isHeader.tsx. Most build servers run Linux, where those are different files, and the build fails with "module not found". Match the import to the exact file name. If you only changed a file's capitalization, git on a case-insensitive system may not record the rename unless you usegit mv. - Node version. Your machine and the build server may run different Node.js versions. On Vercel you can set it in Project Settings → Build and Deployment → Node.js Version, or in the
engines.nodefield ofpackage.json, which overrides the project setting (Vercel: Node.js versions). - Type and build errors. A dev server tolerates things a production build doesn't, such as TypeScript errors in a Next.js build. Step 2 shows you the same failure on your machine.
- Dependencies that only exist on your machine. A package you installed but didn't save to
package.json, or a lockfile that's out of date, builds locally and fails on a clean install.npm ci(Step 2) catches it.
Step 8: Anything on an allow-list needs your new domain
A preview URL, localhost and your live domain are three different origins. Anything configured by origin or URL needs the live one added: auth redirect URLs and OAuth callbacks, CORS settings, webhook endpoints, API-key domain restrictions, and "allowed domains" settings in third-party dashboards. If login or payments are what's failing, the two guides linked in Step 4 are the next stop.
Symptom to likely cause
| What you see | Check first |
|---|---|
| Build fails on the host, works locally | File name case, Node version, clean install with npm ci, type errors (Steps 2 and 7) |
| Page loads, data or features missing | Env vars set for Production and redeployed; NEXT_PUBLIC_*/VITE_* present at build time (Step 3) |
Requests fail in the Network tab, localhost in the URL | Hardcoded localhost (Step 4) |
| "Blocked by CORS policy" in the console | CORS headers or use a same-origin path (Step 5) |
| 404 on refresh or direct link | SPA rewrite (Step 6) |
| 500 from a server route | Runtime logs; usually a missing variable (Steps 1 and 3) |
| Login or payments fail only live | Allow-lists and callbacks (Step 8) |
Quick answers
Why does my app work in the Lovable, Bolt or Cursor preview but not on my own domain?
A preview URL and your own domain are different origins and often different environments. Anything configured per origin or per environment, such as environment variables, auth redirect URLs, OAuth callbacks and CORS settings, needs to be set up for the live one too.
I changed an environment variable and nothing changed. Why?
On Vercel, a change applies only to new deployments, so redeploy. In Next.js and Vite, variables that reach the browser (NEXT_PUBLIC_*, VITE_*) are baked in when the app is built, so they need a new build as well.
Should I keep asking the AI to fix it?
Only with something concrete: the exact error text from the build log, browser console or runtime logs, one problem at a time, with your working version committed to git first. Without the real error, each attempt is a guess, and guesses are how fix-and-break loops start.
Sources
The vendor-specific claims above come from these docs, read on Oct 23, 2026. Docs change, so check the current wording before you rely on a specific setting.
- Vercel: environment variables
- Vercel: Runtime Logs
- Vercel: Vite (SPA rewrites)
- Vercel: supported Node.js versions
- Vercel: how to enable CORS
- Next.js: environment variables
- Vite: env variables and modes
- MDN: Cross-Origin Resource Sharing (CORS)