← All Articles ⚙️ Configuration & DevOps Medium By Nishant Kumar Verma Updated

Hardcoded localhost URLs in Production: A Classic Vibe Coding Bug

Hardcoded http://localhost:3000 breaks on first deploy. Why AI tools write it, and how to fix it with relative paths and NEXT_PUBLIC_ or VITE_ variables.

CFG-002 CFG-005 CFG-006

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:

FrameworkVisible in browser codeRead it with
Next.jsVariables 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 eitherAny variable, never sent to the browserprocess.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
HostWhere production variables live
VercelProject settings, Environment Variables, with separate values for Production, Preview and Development
NetlifyThe site's environment variables settings
RailwayThe service's Variables tab
Fly.iofly secrets set KEY=value
Docker or a VPSdocker 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

  1. Search for the hostnames. Look for localhost and 127.0.0.1 in your source files. A hit inside a string literal that is not an environment fallback is a candidate.
  2. Search for dev ports. :3000, :5173, :8080 and :4000 are usually dev server addresses.
  3. Check your env files. .env.local and other files with real values must be in .gitignore, and nothing with a secret should be committed.
  4. Scan it. VibeDoctor's Vibe Check reports a missing .env.example next to a .env file (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.

Diagnose your codebase - free

VibeDoctor checks for CFG-002, CFG-005, CFG-006 and 148 other issues across 21 diagnostic areas - security, performance, code quality, and more.

SCAN MY APP →