Fix: import.meta.env Undefined on Production Build (Vite)
Vite strips anything not prefixed VITE_ out of the client bundle on purpose — here is how to confirm that's the cause and the three ways to fix it.
TL;DR#
import.meta.env.VITE_SOMETHING returning undefined after vite build
almost always means the variable in your .env file isn't prefixed with
VITE_. Vite only exposes prefixed variables to client code by design — it's
not a bug, and it's the same behaviour in dev, it just tends to go unnoticed
there because of how the value is read.
The error#
A React app built with Vite reads its API URL from an environment variable
that works during vite dev but comes back empty once deployed:
const url = import.meta.env.VITE_SERVER_URL; // undefined after buildThis is one of the most common Vite questions on Stack Overflow
— opening the compiled bundle in DevTools shows the whole import.meta.env
object present, but the specific key you need is missing or empty. The
original question's own code reads three such variables —
VITE_SERVER_URL, VITE_API_ENDPOINT and VITE_AUTH_ENDPOINT — through
import.meta.env, plus import.meta.env.MODE to branch between production
and development, and logs all three with console.log(url, api, auth) for
debugging. All three read correctly during vite dev; only the built,
served output loses them, and Vite's build step raises no warning or error
that would point at the cause.
Why it happens#
Vite's own documentation states the rule directly: "Variables prefixed with
VITE_ will be exposed in client-side source code after Vite bundling." Any
other environment variable — API_KEY, SERVER_URL, DATABASE_URL — is
deliberately left out of import.meta.env in the built bundle. This isn't
an oversight: it stops a .env file that also holds server-side secrets from
leaking straight into a JavaScript bundle every visitor can read.
Three specific mistakes reproduce the same symptom:
- Missing the
VITE_prefix.SERVER_URL=https://api.example.comin.envis invisible toimport.meta.env; onlyVITE_SERVER_URL=...is exposed. - The
.envfile isn't in the project root. Vite looks for.envfiles relative to its environment directory, which defaults to the project root — a file placed insidesrc/is never read. - Reading
process.envinstead ofimport.meta.envin code that runs in the browser. Some older tutorials and CRA-era code useprocess.env.*. Vite doesn't polyfillprocess.envfor client code the way Create React App did, so that pattern silently returnsundefinedfor anything that isn't separately defined.
The gap between dev and production in the original question came from a
fourth cause: manually assigning process.env = { ...process.env, ...loadEnv(mode, process.cwd()) } inside vite.config.ts. That line
patches Node's process.env while Vite's own config is evaluating — it
has no effect on what ends up in import.meta.env inside the bundled
client code, so it can appear to "work" in some dev scenarios where the
config file itself reads process.env, while the actual app code reading
import.meta.env never sees those values in the built output.
Fix#
1. Rename the variable with the VITE_ prefix (the default fix)#
# before
SERVER_URL=https://api.example.com
# after
VITE_SERVER_URL=https://api.example.comconst url = import.meta.env.VITE_SERVER_URL;Rebuild — no vite.config changes needed. This covers the large majority of
reports of this error.
2. Or set a custom prefix with envPrefix#
If renaming every variable isn't practical, Vite lets you change which prefix it looks for:
import { defineConfig } from 'vite';
export default defineConfig({
envPrefix: 'APP_', // now APP_* is exposed instead of VITE_*
});Vite refuses an empty string for envPrefix outright, precisely because
that would expose every environment variable — including ones that were
never meant to reach the browser — to client code.
3. Confirm the .env file location and Vite's build mode#
Make sure the file sits at the project root next to vite.config.ts, not
inside src/, and matches the mode Vite is building for:
.env # loaded in all modes
.env.production # loaded only for `vite build`
.env.development # loaded only for `vite dev`If you need to explicitly load env variables inside vite.config.ts itself
— for example to inject a value via define, rather than relying on the
automatic import.meta.env exposure — use loadEnv with its third
parameter to bypass the prefix filter for that one use case:
import { defineConfig, loadEnv } from 'vite';
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), ''); // '' = ignore prefix, config-time only
return {
define: {
__APP_VERSION__: JSON.stringify(env.npm_package_version),
},
};
});This pattern is for values the config file needs at build time — it still
does not put unprefixed variables into import.meta.env for your
application code, which is a separate, intentional restriction.
4. Stop reading process.env in browser code#
If your code reads process.env.VITE_SERVER_URL instead of
import.meta.env.VITE_SERVER_URL — common in a project migrated from
Create React App, which polyfilled process.env for client code — Vite
won't fill in the same way. Vite's client bundle has no process.env
polyfill by default, so any reference to it in browser-run code returns
undefined regardless of prefix, build mode, or .env file location.
Search the codebase for process.env.VITE_ and replace every match with
import.meta.env.VITE_ — this is a mechanical find-and-replace, not a
config change.
Verify the fix#
Build and preview the production bundle locally rather than trusting dev mode alone, since dev and build can behave differently for the reasons above:
npm run build
npm run previewOpen the app, check the network tab or a console.log(import.meta.env)
temporarily added to your entry file, and confirm the VITE_-prefixed key
now holds the expected value — not undefined. If you're debugging inside a
CI pipeline rather than locally, also check the CI job's own logs for the
vite build step: a missing .env.production file in the CI checkout (as
opposed to a developer's machine, where it might exist untracked) reproduces
the exact same symptom, and is easy to miss if you only ever tested the
build locally where the file was present all along.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
Docker changes where the .env file needs to live#
If the variable is correctly prefixed but still comes back empty inside a
Docker-built image specifically, the usual cause is different: the .env
file wasn't copied into the build context before vite build ran, or the
variable was meant to be injected as a build argument (ARG /
ENV in the Dockerfile) rather than read from a file that only exists on
the host machine. That's a build-context problem, not a Vite prefix problem,
and needs the value passed explicitly at image build time instead.
A related but different Vite failure — the build breaking earlier, before
import.meta.env is even relevant, because index.html was moved into
src/ under a custom root config and Rollup can no longer resolve the
entry script's path — is covered in
Fix Rollup failed to resolve import "/src/main.tsx" in Vite.
If the failure happens even earlier still, before Vite gets to your config at
all, see
Fix Cannot find module '@vitejs/plugin-react' in Vite.
If the project pairs Vite with Next.js-style environment handling instead
(a mixed setup, or a migration in progress), the equivalent Next.js pitfall
is documented in
Next.js Environment Variables Undefined on Vercel? Fix
— the two frameworks expose environment variables through different
mechanisms, so a fix for one does not carry over to the other.
Once the prefix rule clicks, this stops being a recurring surprise: anything
your client bundle needs to read gets VITE_, anything it must never see
stays unprefixed and server-side only.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
Continue Reading
Fix shadcn "No Tailwind CSS configuration found" in Vite
What shadcn init actually looks for before it says Tailwind is missing, reproduced on Tailwind 3 and 4 with the one line that fixes each.
Fix Cannot find module '@vitejs/plugin-react' in Vite
Read one line of the tsc output to tell a missing package from a resolution setting, then fix vite.config.ts in the tsconfig that actually owns it.
Google Fonts not working in Tailwind + Vite: @import order
Why the font-inter class exists but the text stays in the default face in a React and Vite project, and the one-line move that makes the browser download Inter.


