Understanding the Difference Between import.meta.env and process.env

The difference between import.meta.env and process.env is a fundamental part of modern frontend development, but it is also a common source of confusion.

This article compares them from four perspectives: where they come from, where they are used, when their values are determined, and how they affect security.

process.env is the Node.js interface for environment variables, while import.meta.env is the interface Vite provides for exposing environment variables to frontend code.


1. What Is process.env?

Where it comes from

process.env is provided by Node.js. It gives Node applications access to environment variables from the current server, local machine, or build environment.

1
console.log(process.env.NODE_ENV);

It is commonly used in:

  • Node.js services
  • build tools such as Webpack, Vite, and Rollup
  • server-side rendering
  • scripts and backend code

In Node.js, these values can be read from the current process at runtime.

Can it be used in the browser?

Not directly.

Browsers do not provide Node.js’s global process object, so the following code will fail in a native browser environment:

1
ReferenceError: process is not defined

Why does it work in some Webpack projects?

Webpack and similar tools can replace environment variable references during the build.

For example:

1
process.env.NODE_ENV;

may be transformed into:

1
"production";

In this case, the browser is not reading an environment variable at runtime. The build tool has replaced the expression with a fixed value.


2. What Is import.meta.env?

import.meta.env is provided by Vite and is built on top of the standard ES module import.meta syntax.

1
console.log(import.meta.env.MODE);

It does not depend on Node.js’s process object and is designed for modern ESM-based frontend projects.

The distinction is worth noting:

  • import.meta is part of the JavaScript module standard.
  • import.meta.env is a Vite-specific extension.

Its values are available during development and replaced with known values during production builds. Although the application can access them while running, the values themselves are normally determined at build time.


3. Core Differences

Categoryprocess.envimport.meta.env
Provided byNode.jsVite
Main environmentNode.jsVite applications
Available directly in browsersNoAvailable through Vite
Value sourceProcess environmentVite environment files and build configuration
Frontend behaviorUsually replaced by a build toolReplaced by Vite
Recommended for Vite client codeNoYes

Neither one allows frontend code to read private server environment variables at runtime.


4. Why Vite Does Not Use process.env by Default

Vite does not inject a browser-side process global by default.

Using process.env directly in client code can therefore result in an error.

This design avoids adding Node.js globals to the browser environment, keeps frontend code closer to native browser behavior, and makes static analysis and tree shaking easier.

For Vite client code, use import.meta.env instead.


5. The Correct Way to Use Environment Variables in Vite

Custom variables must use the VITE_ prefix

By default, Vite only exposes custom environment variables whose names begin with VITE_.

1
2
# .env
VITE_API_URL=https://api.example.com

They can then be accessed in application code:

1
console.log(import.meta.env.VITE_API_URL);

A variable without the VITE_ prefix will not be exposed to client code by default.

Built-in variables

Vite also provides several built-in values:

1
2
3
4
import.meta.env.MODE;     // development, production, or another mode
import.meta.env.DEV;      // true in development
import.meta.env.PROD;     // true in production
import.meta.env.BASE_URL; // base public path

These values are useful for environment-specific behavior and conditional code.


6. Security

Values exposed through import.meta.env are not private.

They may be included in the JavaScript bundle and can be inspected through browser developer tools.

Do not do this:

1
VITE_SECRET_KEY=xxxx

Frontend environment variables should only contain public configuration, such as:

  • public API URLs
  • feature flags
  • analytics identifiers
  • non-sensitive application settings

Secrets should remain in server-side environment variables:

1
2
process.env.DB_PASSWORD;
process.env.PRIVATE_API_KEY;

A simple rule is:

If a value is available to browser code, treat it as public.


7. How to Handle Environment Variables in SSR and Full-Stack Projects

In a Vite-based SSR project, such as a custom Vite SSR application or a framework built around similar concepts, server and client code use different sources.

Server-side code:

1
process.env.DB_PASSWORD;

Client-side code:

1
import.meta.env.VITE_API_URL;

This separation is intentional.

Why are there two systems?

The server and browser run in different environments.

LocationRuntimeAvailable configuration
SSR serverNode.jsprocess.env
Client bundleBrowserimport.meta.env

A browser cannot securely access private server environment variables.

SSR is not the browser running inside Node.js

A common misunderstanding is that SSR simply runs browser code on the server first.

A more accurate model is:

1
2
3
4
5
Node.js renders HTML
The browser loads the page
Client-side code hydrates the HTML

These two stages have different runtimes, different sources of configuration, and different security boundaries.

How values flow in Vite SSR

On the server:

  • values can be read from the Node.js process
  • private values remain on the server
  • server-only values should not enter the client bundle

In the browser:

  • public values are exposed by Vite
  • values are included in or referenced by the client bundle
  • users can inspect them

SSR does not automatically pass server environment variables to the browser, and it should not do so.


8. Three Common SSR Mistakes

Mistake 1: Using process.env directly in shared code

1
2
3
// utils/config.ts
// Imported by both server and client code
export const API_URL = process.env.API_URL;

This may work on the server but fail in the browser or be transformed incorrectly during the build.

A runtime check is sometimes used:

1
2
3
4
export const API_URL =
  typeof window === "undefined"
    ? process.env.INTERNAL_API_URL
    : import.meta.env.VITE_API_URL;

However, a clearer approach is to keep server and client configuration in separate modules. This makes the runtime boundary easier to understand and reduces the chance of exposing private values.

Mistake 2: Reading database variables inside a component

1
console.log(process.env.DB_PASSWORD);

A Vue or React component used in SSR may still be included in client-side code and run again during hydration.

Do not access private server values from code that may reach the browser.

Mistake 3: Treating environment variables as runtime configuration

Client-side Vite variables are normally determined during the build:

1
2
3
4
5
6
7
Build-time value
JavaScript bundle
CDN cache
All users receive the same value

If configuration must change without rebuilding the application, use a runtime configuration mechanism instead.

Common options include:

  • returning configuration from an API
  • injecting configuration into HTML
  • loading a JSON configuration file

For example:

1
2
3
4
5
<script>
  window.CONFIG = {
    apiUrl: "https://api.example.com",
  };
</script>

or:

1
2
3
const config = await fetch("/config.json").then((response) =>
  response.json(),
);

Anything sent to the browser is still public, even when it is loaded at runtime.


9. A Practical SSR Architecture

A typical configuration boundary looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
┌──────────────────────────┐
│      Browser Client      │
│ import.meta.env.VITE_*   │  Public configuration
└────────────▲─────────────┘
         HTTP / HTML
┌────────────┴─────────────┐
│      Node SSR Server     │
│      process.env.*       │  Private configuration
└────────────▲─────────────┘
       Internal access
┌────────────┴─────────────┐
│     DB / Redis / OSS     │
└──────────────────────────┘

The browser communicates with the server through HTTP. The server then accesses databases, storage, and other private infrastructure.

Private configuration should remain inside that server-side boundary.


10. Nuxt and Runtime Configuration

Nuxt provides its own configuration layer to help maintain the same boundary.

ConfigurationPurpose
runtimeConfigServer-only configuration
runtimeConfig.publicConfiguration exposed to the client
process.envServer environment variables

Nuxt is essentially helping the application distinguish between private server configuration and public client configuration.


11. Common Misunderstandings

import.meta.env reads values from the server at runtime

It does not. Client-facing values are normally determined during the Vite build.

import.meta.env can dynamically switch environments

Not by itself. Changing a build-time environment variable usually requires a new build.

Use an API, an injected configuration object, or a runtime JSON file when configuration must change after deployment.

process.env can still be used everywhere in Vite

Not in browser code by default.

It can still be used in Node.js scripts, build configuration, SSR server code, and other server-only modules. Adding a browser polyfill is possible, but it is usually unnecessary and can make the client-server boundary less clear.


Conclusion

The basic rule is straightforward:

1
2
Vite client code  → import.meta.env.VITE_*
Node.js code      → process.env

Keep private values on the server, and never place secrets in environment variables that are exposed to frontend code.

The most important distinction is not the syntax. It is the runtime in which the code executes and whether the value is allowed to reach the browser.

Licensed under CC BY-NC-SA 4.0