Guides

Custom modes (staging, qa)

Surface non-default Vite modes to browser code. Why the envConfig() plugin is required even if you load config via dynamic import.

The problem

You run vite build --mode staging and expect envName() to say "staging" in the browser. It won't — and the reason is subtle. In the browser, envName()'s readEnv() only reads window.__env (or the <EnvScript /> tag), never process.env. A pure SPA has neither, so NODE_ENV and VITE_ENV are invisible and envName() falls through to its "development" fallback — no matter what --mode you built with. (Vite even forces NODE_ENV="production" at build time, but that only ever reaches envName() on the server, not in browser code.)

This is independent of how you load config: it bites both Pattern 1 (dynamic import) and any browser code that calls envName() directly.

The fix

The envConfig() plugin injects __ENV_NAME__ = JSON.stringify(mode) as a build-time constant. envName() checks __ENV_NAME__ before NODE_ENV in its precedence chain, so the browser sees the mode you actually built.

vite.config.ts
import { envConfig } from "@vlandoss/env/vite";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [envConfig()],
});

That's all the plugin needs to do for this case — you don't have to import #config or do anything else.

Building and running custom modes

terminal
vite build --mode staging
vite build --mode qa
vite build --mode production

Place a matching config file per mode under [src/]config/:

src/
  config/
    development.ts
    staging.ts
    qa.ts
    production.ts

The plugin's discovery scans [src/]config/<mode>.{ts,mts,cts,js,mjs,cjs,json} and pulls in the right one — same algorithm as loadConfig in @vlandoss/env/fs.

Selecting the env without --mode

If you'd rather not thread --mode through every command, set a VITE_ENV env var instead. The plugin reads it from process.env and your .env* files (via Vite's loadEnv, so an inline/shell value wins over a file value) and uses it to pick the config file and __ENV_NAME__. These are equivalent:

terminal
VITE_ENV=staging vite build
vite build --mode staging

VITE_ENV takes precedence; when it's unset or empty the plugin falls back to Vite's mode, so --mode keeps working unchanged. Put it in a .env file to make it the default for a project:

.env.staging
VITE_ENV=staging

Rename the var with the envVar option if VITE_ENV clashes with something:

vite.config.ts
export default defineConfig({
  plugins: [envConfig({ envVar: "APP_ENV" })],
});

Verifying it works

After a vite build --mode staging, anywhere in browser code:

import { envName } from "@vlandoss/env";

console.log(envName());  // "staging"

And env.$name on the resolved env object will also be "staging".

Reference example

examples/spa-vite-dynamic/ wires the plugin purely for the __ENV_NAME__ inject — its config is loaded by a dynamic import(), not by the #config alias. The plugin earns its place in the config solely to make envName() honest after a build.

Server vs. browser

On the server, envName() reads process.env.ENV (highest precedence) or process.env.NODE_ENV. There's no __ENV_NAME__ involved — Node doesn't have Vite's NODE_ENV quirk. So custom modes "just work" on the server side as long as you set ENV=staging (or NODE_ENV=staging) before running the process.

On this page