Environment variable undefined after deploy? Fix VITE_, NEXT_PUBLIC_ and missing env vars on Vercel and Netlify
Your app works on localhost, but the deployed version shows a blank page, "supabaseUrl is required." in the console, or API calls going to undefined/api/…. Usually an environment variable that exists on your machine doesn't exist in the build on Vercel or Netlify, or exists but the browser can't see it. This guide covers the three causes we see in almost every Lovable, Bolt and Cursor app: the variable was never added to the host, it has the wrong prefix, or the site wasn't redeployed after adding it.
- Confirm it's really a missing env var
- Know when and where the variable is read
- Use the right prefix and syntax for your framework
- Add the variables on Vercel or Netlify (your .env file isn't deployed)
- Redeploy after every env var change
- Next.js: when NEXT_PUBLIC_ is set but still undefined
- Vite: config, modes and string values
- Keep secrets on the server (and don't 'fix' it by making them public)
- Lovable, Bolt and Cursor: find every variable the app needs
- FAQ
1. Confirm it's really a missing env var
Open the live site, then DevTools → Console. These messages almost always mean a variable is undefined in the deployed build:
Uncaught Error: supabaseUrl is required.
Error: supabaseKey is required.
Invalid supabaseUrl: Must be a valid HTTP or HTTPS URL.
Neither apiKey nor config.authenticator provided // Stripe SDK
ReferenceError: process is not defined // process.env in a Vite app
GET https://yourapp.com/undefined/api/items 404 // API base URL missing
A blank white page with one of these errors means the app crashed while creating a client at startup (Supabase, Stripe, Firebase, OpenAI). Check the host's build log too: Next.js and server code may fail during the build instead.
To check what the browser actually received without leaking anything, open DevTools → Sources, press Ctrl/Cmd+Shift+F and search the bundled JS for a piece of the value you expect (for example your Supabase project ref abcd1234.supabase.co). If it isn't there, the value was never baked into the build. Locally, npm run build and then grep -r "abcd1234" dist/ (Vite) or .next/static (Next.js) does the same check.
2. Know when and where the variable is read
Most "it's set but still undefined" confusion comes from mixing up two different kinds of variables:
- Browser (client) variables are baked in at build time. Vite replaces
import.meta.env.VITE_…and Next.js replacesprocess.env.NEXT_PUBLIC_…with the literal value while building. The browser has no environment of its own, so if the variable wasn't present during that build, the bundle containsundefinedforever, even if you add it to the host afterwards. - Server variables are read at runtime. API routes, Route Handlers, Vercel/Netlify Functions and Supabase Edge Functions read
process.env.X(orDeno.env.get('X')) when the function runs. They never reach the browser, which is exactly where secret keys belong.
So first decide: is the failing code running in the browser (a React component, a supabase client file in src/) or on the server (an api/ folder, route.ts, a function)? The fix is different for each.
3. Use the right prefix and syntax for your framework
Bundlers only expose variables with a public prefix to browser code. Without it, the variable is deliberately left out and reads as undefined:
- Vite (Lovable, Bolt and most React apps): name it
VITE_SUPABASE_URLand read it withimport.meta.env.VITE_SUPABASE_URL. Vite's docs showimport.meta.env.DB_PASSWORDasundefinedon purpose. Don't useprocess.envin Vite client code: that's what causesprocess is not defined. - Next.js: browser code needs
NEXT_PUBLIC_, e.g.process.env.NEXT_PUBLIC_SUPABASE_URL. Unprefixed variables only exist on the server. - Create React App:
REACT_APP_andprocess.env.REACT_APP_…. Astro:PUBLIC_.
# .env (Vite)
VITE_SUPABASE_URL=https://abcd1234.supabase.co
VITE_SUPABASE_ANON_KEY=eyJ... # publishable / anon key only
// src/lib/supabase.ts
const url = import.meta.env.VITE_SUPABASE_URL
const key = import.meta.env.VITE_SUPABASE_ANON_KEY
if (!url || !key) throw new Error('Missing VITE_SUPABASE_URL or VITE_SUPABASE_ANON_KEY')
That explicit check turns a vague blank page into an error that names the missing variable. The name in your code, your .env file and the host dashboard must match exactly, including case: VITE_SUPABASE_ANON_KEY and VITE_SUPABASE_KEY are different variables.
4. Add the variables on Vercel or Netlify (your .env file isn't deployed)
Your .env / .env.local file is normally in .gitignore, so it never reaches GitHub and never reaches the host. That's the single most common reason an app "works locally but not in production". Add every variable on the host:
- Vercel: Project → Settings → Environment Variables. Add each name and value and tick the environments it applies to: Production for your live domain, Preview for branch/PR deployments. A variable set only for Preview or Development is
undefinedin Production. From the CLI,vercel env lslists what's set andvercel env pulldownloads the Development values into a local file. - Netlify: Site configuration → Environment variables. By default a variable applies to every deploy context; if you've set per-context values or scopes, make sure Production has a value and the Builds scope is included for browser variables (and Functions for server-side keys). Variables in
netlify.tomloverride the ones in the UI.
Paste values exactly: no surrounding quotes, no trailing spaces or line breaks. In a dashboard the quotes become part of the value, so "https://abcd1234.supabase.co" fails as an invalid URL. On Vercel, values saved as Secret can't be read back after saving, so if you're unsure what you pasted, re-enter it.
5. Redeploy after every env var change
Vercel's docs are explicit: environment variable changes are not applied to previous deployments, only to new ones, so you must redeploy. The same is true on Netlify. Adding the variable and refreshing the live site changes nothing, because the old build still contains undefined.
- Vercel: Deployments → the latest production deployment → ⋯ → Redeploy, or push a new commit to your production branch.
- Netlify: Deploys → Trigger deploy → Deploy site (or Clear cache and deploy site if you also changed build settings).
Then hard-refresh the live site (Ctrl/Cmd+Shift+R) and repeat the Sources search from step 1. Browser variables are frozen into each build, so if you build once and promote the same build or Docker image to another environment, it keeps the values from the original build.
6. Next.js: when NEXT_PUBLIC_ is set but still undefined
Next.js only inlines NEXT_PUBLIC_ variables when the code reads them directly as process.env.NEXT_PUBLIC_NAME. Its docs list dynamic lookups that are not inlined, and AI-written config helpers love them:
// ✅ inlined at build time
const url = process.env.NEXT_PUBLIC_SUPABASE_URL
// ❌ NOT inlined: undefined in the browser
const name = 'NEXT_PUBLIC_SUPABASE_URL'
const url = process.env[name]
const env = process.env
const url = env.NEXT_PUBLIC_SUPABASE_URL
- Server variables (no prefix) work in Route Handlers, Server Actions and server components, but are
undefinedinside'use client'components. Read them on the server and pass the result down, or call an API route. - Pages that are statically rendered read
process.envduringnext build, so they also need a redeploy after a change. For values that must be read per request, Next.js documents opting into dynamic rendering (for example withawait connection()). - Keep
.env*files in the project root, not in/src: Next.js only loads them from the root.
7. Vite: config, modes and string values
vite.config.tsdoesn't see your.envautomatically. If the config itself needs a value (a proxy target, a base path), load it withloadEnv(mode, process.cwd(), '')fromvite.- Modes:
vite buildruns inproductionmode and loads.env.production, which overrides.env. A stale.env.productionin the repo can overwrite the right value. Variables already set in the environment (like the ones Vercel/Netlify inject) take priority over all.envfiles. - Everything is a string.
VITE_FEATURE_X=falsegives the string"false", which is truthy. Compare with=== 'true'. - Restart the dev server after editing
.env: Vite only reads it on startup. - Only static references are replaced: write
import.meta.env.VITE_API_URLin full rather than building the name from a string.
8. Keep secrets on the server (and don't 'fix' it by making them public)
The tempting fix for an undefined STRIPE_SECRET_KEY or OPENAI_API_KEY in a React component is to rename it VITE_… or NEXT_PUBLIC_…. Don't. Anything with a public prefix is pasted into JavaScript that every visitor downloads. Vite's docs warn that VITE_* variables should not contain sensitive information.
- Safe in the browser: Supabase URL and anon/publishable key (with RLS on), Stripe publishable key, analytics IDs.
- Server only: Supabase service role/secret key, Stripe secret key, OpenAI/Anthropic keys, database URLs, webhook secrets. Use them from an API route, a Vercel/Netlify Function or a Supabase Edge Function. For Edge Functions, set them with
supabase secrets set NAME=value(or in the dashboard) and readDeno.env.get('NAME'); a missing secret there often shows up in the browser as a CORS error (see Supabase Edge Function CORS error?). - If a secret key was ever deployed with a public prefix, assume it's leaked: rotate it in the provider's dashboard, then update the host and redeploy.
9. Lovable, Bolt and Cursor: find every variable the app needs
AI builders often set variables in their own preview environment that never make it into your export. Before deploying anywhere else, list every variable the code reads:
grep -rn "import.meta.env\|process.env\|Deno.env" src app pages lib supabase/functions 2>/dev/null
- Lovable: on Lovable hosting it's handled for you. If you export the GitHub repo and deploy to Vercel or Netlify, open the file that calls
createClient(usuallysrc/integrations/supabase/client.ts) to see whether it reads env vars or hardcoded values, and add those exact names on the host. More publish problems: Lovable app broken after publishing? - Bolt: Bolt's preview has its own
.env. When you deploy to Netlify, everyVITE_variable must be added in Netlify too. See Bolt.new app not working on Netlify? - Cursor / Claude Code: ask it to create a
.env.examplelisting every variable name with no values, and to add a startup check that throws a clear error for any missing one.
A prompt that works: "List every environment variable this app reads, whether each is used in the browser or on the server, and the exact name it needs on Vercel. Rename any browser variable to use the VITE_ prefix, move any secret key out of client code into a server function, and add a startup check for missing variables."
Still broken after a redeploy? Send us your live URL and the console error through the free check form or by email to fix@getvibefix.com. We'll tell you exactly what's wrong for free, before you pay anything.
FAQ
Why is my environment variable undefined on Vercel when it works locally?
Usually one of three things: the variable is only in your local .env file (which is gitignored and never deployed), it isn't enabled for the Production environment in Vercel's Settings → Environment Variables, or you added it without redeploying. Browser code also needs the VITE_ (Vite) or NEXT_PUBLIC_ (Next.js) prefix.
Do I need to redeploy after changing environment variables on Vercel or Netlify?
Yes. Vercel applies environment variable changes only to new deployments, and Netlify likewise uses them on the next build. Browser variables are baked into the JavaScript at build time, so redeploy and hard-refresh before testing.
Why is import.meta.env.VITE_SOMETHING undefined?
Check that the name starts with VITE_ (or your custom envPrefix), that it is set on the host for the build that is live, that the site was redeployed after adding it, and that you read it with the full static name import.meta.env.VITE_SOMETHING. Use import.meta.env, not process.env, in Vite client code.
Why is process.env.NEXT_PUBLIC_ variable undefined in the browser?
It wasn't present when next build ran (add it on the host and redeploy), the name is missing the NEXT_PUBLIC_ prefix, or the code reads it dynamically, e.g. process.env[name] or via a destructured copy of process.env. Next.js only inlines direct process.env.NEXT_PUBLIC_NAME references.
Is it safe to put an API key in a VITE_ or NEXT_PUBLIC_ variable?
Only if it's designed to be public, like the Supabase anon/publishable key or a Stripe publishable key. Prefixed variables are bundled into code every visitor can read. Secret keys (Stripe secret, OpenAI, Supabase service role) must stay in server code, functions or Edge Function secrets, and should be rotated if they were ever exposed.
Still stuck? We'll fix it at a flat price.
VibeFix fixes broken Lovable, Bolt, Cursor, Replit and v0 apps. Request a free check first, and we'll tell you what's wrong and which plan fits before you pay. If we can't fix it, you get a full refund.
Already know you need help? Quick Fix, $49 (one bug, 24h) · Standard Rescue, $129 · Deploy-Ready, from $199