Quick Answer
A hardcoded http://localhost:3000 works on your machine and fails everywhere else, because localhost always means "this computer". In production the browser tries to reach the user's own computer, and the request fails with ERR_CONNECTION_REFUSED. Use a relative path such as /api/users when the API is on the same domain, and an environment variable such as NEXT_PUBLIC_API_URL or VITE_API_URL when it is not.
Why AI-Generated Code Hardcodes localhost
Bolt, Lovable, Cursor and v0 write code that runs during the session, and during the session your app lives at http://localhost:3000 (Next.js) or http://localhost:5173 (Vite). A concrete URL makes the demo work at once, so the address ends up in fetch calls, CORS settings and webhook registrations.
Nothing fails until the first deployment. Then every call to that address fails, because in a user's browser localhost is the user's own computer, and on your server it is the server itself.
The Patterns AI Tools Generate
// BAD: hardcoded localhost in a fetch call
const response = await fetch('http://localhost:3000/api/users');
// BAD: the same address in a constant (easier to fix, still wrong)
const API_BASE = 'http://localhost:3000';
const users = await fetch(`${API_BASE}/api/users`);
// BAD: hardcoded in a Next.js route handler
export async function POST(req: Request) {
const webhookUrl = 'http://localhost:3000/api/webhook/stripe';
await registerWebhook(webhookUrl);
}
// BAD: CORS that only allows your dev server
app.use(cors({ origin: 'http://localhost:3000' }));
// BAD: a local service address
const SUPABASE_URL = 'http://localhost:54321';
In production the fetch calls fail with network errors that show up in the browser console, the CORS setting rejects every request from your real domain, and the Supabase URL points at a local Docker container that does not exist on the server.
The Fix: Relative Paths First, Environment Variables Second
If the frontend and the API are deployed together, as in a Next.js app with route handlers, you do not need a URL at all. A relative path works in development and production:
// GOOD: same-origin call, no hostname needed
const response = await fetch('/api/users');
For a separate API or service, read the URL from an environment variable. Which prefix makes it visible in the browser depends on your framework:
| Framework | Visible in browser code | Read it with |
|---|---|---|
| Next.js | Variables prefixed NEXT_PUBLIC_ | process.env.NEXT_PUBLIC_API_URL |
| Vite (Bolt, Lovable and most React starters) | Variables prefixed VITE_ | import.meta.env.VITE_API_URL |
| Server code in either | Any variable, never sent to the browser | process.env.APP_URL |
// GOOD: Next.js client code, with a local default for development
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:3000';
// GOOD: Vite client code
const API_BASE = import.meta.env.VITE_API_URL;
// GOOD: server-side, fail loudly if the variable is missing
export async function POST(req: Request) {
const appUrl = process.env.APP_URL;
if (!appUrl) throw new Error('APP_URL is not set');
await registerWebhook(`${appUrl}/api/webhook/stripe`);
}
// GOOD: CORS origin from the environment
app.use(cors({ origin: process.env.ALLOWED_ORIGIN ?? 'http://localhost:3000' }));
Never give a secret a public prefix. A service-role key or API secret named NEXT_PUBLIC_... or VITE_... is copied into the JavaScript every visitor downloads. See client-side secret exposure for how that happens.
Where to Set the Production Values
# .env.local (your machine only, listed in .gitignore)
NEXT_PUBLIC_API_URL=http://localhost:3000
APP_URL=http://localhost:3000
# Production values go in your host's settings, not in a committed file:
# NEXT_PUBLIC_API_URL=https://api.your-app.com
# APP_URL=https://your-app.com
| Host | Where production variables live |
|---|---|
| Vercel | Project settings, Environment Variables, with separate values for Production, Preview and Development |
| Netlify | The site's environment variables settings |
| Railway | The service's Variables tab |
| Fly.io | fly secrets set KEY=value |
| Docker or a VPS | docker run -e KEY=value, or an env_file in Docker Compose |
Next.js and Vite both copy public variables into the bundle when you build, so change the value in your host's settings and redeploy for it to take effect.
Setting production values is one step of a release. Our guide to deploying a vibe-coded app to production walks through the rest, from HTTPS and security headers to monitoring.
Commit a .env.example
AI tools rarely create one. A committed .env.example lists every variable the app needs, with placeholders instead of real values, so the next deployment does not miss one:
# .env.example (committed; placeholders only)
NEXT_PUBLIC_API_URL=https://your-app-domain.com
APP_URL=https://your-app-domain.com
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
STRIPE_SECRET_KEY=your-stripe-secret-key
How to Find Hardcoded URLs in Your Codebase
- Search for the hostnames. Look for
localhostand127.0.0.1in your source files. A hit inside a string literal that is not an environment fallback is a candidate. - Search for dev ports.
:3000,:5173,:8080and:4000are usually dev server addresses. - Check your env files.
.env.localand other files with real values must be in.gitignore, and nothing with a secret should be committed. - Scan it. VibeDoctor's Vibe Check reports a missing
.env.examplenext to a.envfile (CFG-005) in any project, and hardcoded localhost URLs in Python code (CFG-002). For JavaScript and TypeScript, the search in step 1 is still the way to find them. Free to sign up.
FAQ
Why does my app work locally but fail after deploying?
Often because a URL in the code still points at localhost. On your machine that is your dev server; in production it is the user's own computer, so the request fails. Replace it with a relative path or an environment variable and redeploy.
Should I use the NEXT_PUBLIC_ prefix for my API URL?
Only if the call runs in the browser. Client components need NEXT_PUBLIC_API_URL. Server Components, Server Actions and route handlers can read an unprefixed variable such as API_URL. Never put the prefix on a secret, because it ends up in the browser bundle.
How do I use environment variables in a Vite, Bolt or Lovable app?
Name the variable with the VITE_ prefix, such as VITE_API_URL, and read it with import.meta.env.VITE_API_URL. Vite only exposes VITE_ variables to browser code; others read as undefined. Put local values in .env.local and production values in your host's settings, then rebuild.
Does Vercel set a URL variable automatically?
Yes, once "Enable access to System Environment Variables" is on in the project settings. VERCEL_URL is the address of the current deployment and changes with every deploy. VERCEL_PROJECT_PRODUCTION_URL is your production domain and stays the same, even in previews. Neither includes https://, so add it yourself.
Can I use relative URLs instead of environment variables?
Yes, for calls to the same domain. A fetch to /api/users works in development and production because the browser resolves it against the current site. You need an absolute URL only for a service on a different domain, or for server code that must build a full link, such as a webhook or an email.
What about Supabase local development?
Supabase's local stack runs on http://localhost:54321 by default. Point .env.local at it and set your production project URL, on supabase.co, in your host's settings. Commit only .env.example with placeholders.