DEV Community

Cover image for Fix: import.meta.env Undefined on Production Build (Vite)
Mahdi BEN RHOUMA
Mahdi BEN RHOUMA

Posted on Originally published at iloveblogs.blog

Fix: import.meta.env Undefined on Production Build (Vite)

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:

```javascript title="config.ts"
const url = import.meta.env.VITE_SERVER_URL; // undefined after build




[This is one of the most common Vite questions on Stack Overflow](https://stackoverflow.com/questions/67378099/import-meta-env-undefined-on-production-build-vitejs)
— 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:

1. **Missing the `VITE_` prefix.** `SERVER_URL=https://api.example.com` in
   `.env` is invisible to `import.meta.env`; only `VITE_SERVER_URL=...`
   is exposed.
2. **The `.env` file isn't in the project root.** Vite looks for `.env`
   files relative to its [environment directory](https://vite.dev/config/shared-options.html#envdir),
   which defaults to the project root — a file placed inside `src/` is never
   read.
3. **Reading `process.env` instead of `import.meta.env` in code that runs in
   the browser.** Some older tutorials and CRA-era code use `process.env.*`.
   Vite doesn't polyfill `process.env` for client code the way Create React
   App did, so that pattern silently returns `undefined` for 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)



```bash title=".env"
# before
SERVER_URL=https://api.example.com

# after
VITE_SERVER_URL=https://api.example.com
Enter fullscreen mode Exit fullscreen mode

```javascript title="config.ts"
const 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:



```javascript title="vite.config.ts"
import { defineConfig } from 'vite';

export default defineConfig({
  envPrefix: 'APP_', // now APP_* is exposed instead of VITE_*
});
Enter fullscreen mode Exit fullscreen mode

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`
Enter fullscreen mode Exit fullscreen mode

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:

```javascript title="vite.config.ts"
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:



```bash
npm run build
npm run preview
Enter fullscreen mode Exit fullscreen mode

Open 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.

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.


Originally published at https://www.iloveblogs.blog

Top comments (0)