Guides

My app works in preview but breaks in production (environment variables differ)

Short answer: production is reading different settings than your preview. Hosts like Vercel and Netlify keep a separate set of environment variables for each environment, so a key you added for Preview isn't automatically there for Production. Add the missing value to the Production environment, then redeploy: changes only apply to new deployments. If the value is used in the browser, it also needs the right prefix (VITE_ or NEXT_PUBLIC_) and is baked in when the app is built. Never put a secret key in one of those public variables.

Signs this is your problem

Why preview and production differ

An environment variable is a named setting that lives outside your code, like SUPABASE_URL or STRIPE_SECRET_KEY. Your host stores them per environment:

So "it works in preview" often means "the value exists in one place but not the other", or it exists in both with different values, such as a test key in one and a live key in the other.

Fix it, step by step

  1. List every variable your app reads. Search your code for import.meta.env., process.env. and Deno.env.get, or ask your builder to list them.
  2. Open your host's environment variable settings and check each one is set for Production, not just Preview or Development. Watch for typos and extra spaces: VITE_SUPABASE_URL and VITE_SUPABASE_URL are different names.
  3. Check the values make sense for a live site: your real domain instead of a preview link or localhost, and live keys where real customers pay.
  4. Redeploy production. Vercel says variable changes don't apply to previous deployments, only new ones. Saving the setting isn't enough: start a new production deployment.
  5. Open the live site in a private window and try the thing that was broken.

Browser variables are baked in at build time

Code that runs in your visitor's browser can't read your host's settings while it runs. Instead, the build copies the values into the JavaScript files:

That's why a browser value you fix in the dashboard stays wrong until you rebuild and redeploy.

Never fix it by making a secret public

It's tempting to rename STRIPE_SECRET_KEY to VITE_STRIPE_SECRET_KEY to make an error go away. Don't. Anything with a VITE_ or NEXT_PUBLIC_ prefix ends up inside files that anyone can download from your site. Vite's own docs warn that these variables should not contain sensitive information such as API keys.

If a secret key has already been in a public variable on a live site, assume it was seen: rotate it in that service's dashboard and update the new value on the server.

Test keys vs live keys

Stripe's sandbox (test) and live mode have separate keys, and objects in one mode can't be used in the other. A common mix-up: the Production environment gets pk_live_ in the browser but still has sk_test_ on the server, or the other way round, so checkout fails. Use test keys everywhere in Preview, live keys everywhere in Production, and copy the live webhook signing secret too, because each webhook endpoint has its own. If paid customers aren't being unlocked, see Stripe says they paid, but nothing unlocked.

Addresses that differ: the site URL

Many apps keep their own address in a variable, like VITE_SITE_URL or NEXT_PUBLIC_SITE_URL, and use it for sign-up, password-reset and payment-return links. If Production still has the preview address or localhost, people get sent there. Set it to your real domain in Production. In Supabase, also set the Site URL under Authentication → URL Configuration to your real domain and add it to Redirect URLs; Supabase's docs say to change the Site URL from http://localhost:3000 to your production URL. Our guide Login broke after I added my custom domain walks through that part.

Built with Lovable, Bolt, Cursor, or by hand?

The same rules apply wherever the code came from. You can paste this to your AI builder:

My app works on the preview link but not on my live site. Please:
1. List every environment variable the app reads, and say whether each one is used in the browser or only on the server.
2. Make sure no secret (Stripe sk_ or rk_ key, webhook secret, Supabase service role key, any other API key) uses a VITE_ or NEXT_PUBLIC_ prefix or is written into front-end code. Move any that are into server-side code.
3. Replace hardcoded preview URLs and localhost in auth and payment code with a site-URL variable or window.location.origin.
4. Tell me exactly which variables I need to set for Production in my host's dashboard, without printing any secret values.

Then set those values for Production yourself and redeploy.

How AppFirstAid helps

Our free check reads your live site from the outside, like a visitor's browser does. It looks for secret keys that have ended up in your public JavaScript, and for sign-in, email-confirmation or payment-return addresses in that code that point at localhost or a preview or test address instead of your live domain, which is a common sign a production setting was never updated. It can't see your host's dashboard or server-only settings. A journey test goes further: a real browser signs up, opens the confirmation email, logs in and tries checkout on your live site, and shows you the step that broke.

Sources, read October 27, 2026: Vercel docs "Environment variables" (Production, Preview and Development scopes, branch overrides, changes only apply to new deployments); Vite docs "Env Variables and Modes" (VITE_ prefix, replaced at build time, don't put secrets in them); Next.js docs "Environment variables" (NEXT_PUBLIC_ inlined and frozen at build time, dynamic lookups not inlined); Netlify docs "Build environment variables" (deploy contexts, builds don't read .env files); Stripe docs "API keys" (sandbox vs live keys, only publishable keys are safe to expose); Supabase docs "Redirect URLs" (Site URL and redirect allow list). These tools may change these details.

Check your live app in under a minute

AppFirstAid's free check looks at your live site from the outside: leaked keys, files left in public, safety settings, and whether your sign-up, login and checkout pages open. No signup. Journey tests that sign up, log in and try checkout like a real customer are coming soon (email us to hear when).

Run the free check

Settings move around as these tools update. If a menu name has changed, look for the same words nearby. Spotted something out of date? Tell us.

More guides