# Custom modes (staging, qa) (/docs/guides/custom-modes)



## The problem [#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), &#x2A;*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)](/docs/guides/spa-dynamic-import) and any browser code that calls `envName()` directly.

## The fix [#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](/docs/concepts/env-name), so the browser sees the mode you actually built.

```ts title="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 [#building-and-running-custom-modes]

```bash title="terminal"
vite build --mode staging
vite build --mode qa
vite build --mode production
```

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

```text
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` [#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:

```bash title="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:

```dotenv title=".env.staging"
VITE_ENV=staging
```

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

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

## Verifying it works [#verifying-it-works]

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

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

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

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

## Reference example [#reference-example]

[`examples/spa-vite-dynamic/`](https://github.com/variableland/env/tree/main/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 [#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.
