---
title: Environments
description: Best practices for managing multiple environments with varlock
---

One of the main benefits of using environment variables is the ability to boot your application with configuration intended for different environments (e.g., development, preview, staging, production, test).

You may use both [functions](/reference/functions/) and/or environment-specific `.env` files (e.g., `.env.production`) to alter configuration accordingly in a declarative way. Plus the additional guardrails provided by `varlock` also make this much safer no matter where values come from.

:::tip[environment-specific files are optional]
While many have traditionally shied away from using environment-specific `.env` files due to fear of committing sensitive values, the ability to set values using [plugins](/guides/plugins/) makes it easier to securely, and collaboratively, manage these values.

:::


### Process overrides
`varlock` will always treat environment variables passed into the process with the most precedence. Generally, we recommend moving as much configuration as possible into your `.env` files, but there are cases where you may want to override specific values at runtime, either from the environment itself, or by prepending them to your command (e.g., `APP_ENV=prod pnpm run build`).

At the very least, you'll often need to inject an environment flag (e.g., `APP_ENV`) and a _secret-zero_ which allows access to the rest of your secrets in deployed environments. For local workflows, consider [device-local encryption](/guides/local-encryption/) to secure local secret-zero handling.

That said, as a first step to adopting `varlock`, you could rely entirely on process overrides to inject all config values, but still benefit from having a clear schema with validation applied to them.

### Loading environment-specific `.env` files

Any environment-specific files (e.g., `.env.development`) will automatically be loaded if they match the value of the _current environment_ as set by the [`@currentEnv`](/reference/root-decorators/#currentenv) root decorator in your `.env.schema` file.

The files are applied with a specific precedence (increasing):
- `.env.schema` - your schema file, which can also contain default values
- `.env` - ⚠️ **not recommended**, will be loaded but better to use others
- `.env.local` - local overrides _(gitignored)_
- `.env.[currentEnv]` - environment-specific values
- `.env.[currentEnv].local` - environment-specific local overrides _(gitignored)_

:::tip[Auto-detect with `VARLOCK_ENV`]
Instead of managing your own environment flag, you can use the built-in `$VARLOCK_ENV` variable which auto-detects the environment from your CI/deploy platform. See the [builtin variables reference](/reference/builtin-variables/) for details.

```env-spec title=".env.schema"
# @currentEnv=$VARLOCK_ENV
```
:::

For example, consider the following `.env.schema`:
```env-spec title=".env.schema"
# @currentEnv=$APP_ENV
# ---
# @type=enum(development, test, staging, production)
APP_ENV=development
```

Your environment flag key is set to `APP_ENV`, which has a default value of `development` - meaning that `.env.development` and `.env.development.local` will be loaded if they exist.

To tell `varlock` to load `.env.staging` instead, you must set `APP_ENV` to `staging` - usually using an override passed into the process. For example:

```bash
APP_ENV=staging varlock run -- node my-test-script.js
```

:::note[Loading `.env.local` in `test` environment]
Some tools ([dotenv-flow](https://github.com/kerimdzhanov/dotenv-flow), [Next.js](https://nextjs.org/docs/pages/guides/environment-variables#test-environment-variables), etc) make a special exception
to skip loading `.env.local` if the current environment is `test`. Others tools ([Vite](https://vite.dev/config/#env-variables)) do not have any special handling.

We chose to follow Vite's lead, and instead provide a way to explicitly opt-in to that behavior:

```env-spec title=".env.local"
# @disable=forEnv(test)
```
:::

:::note[Next.js precedence order]
Unlike Varlock (which matches Vite and dotenv-flow), [Next.js](https://nextjs.org/docs/pages/guides/environment-variables#environment-variable-load-order) swaps the order of precedence for `.env.local` vs `.env.[currentEnv]`.
:::

## Advanced logic using functions

On some platforms, you may not have full control over a build or boot command or the env vars passed into them.
In this case, we can use functions to transform other env vars provided by the platform into the environment flag value we want.
We can use [`remap()`](/reference/functions#remap) to transform a value according to a lookup, along with [regex literals](/reference/functions#regex-literals) if we need to match a pattern instead of an exact value.

For example, on the Cloudflare Workers CI platform, we get the current branch name injected as `WORKERS_CI_BRANCH`, which we can use to determine which environment to load:

```env-spec title=".env.schema"
# @currentEnv=$APP_ENV
# ---
# set to current branch name when build is running on Cloudflare CI, empty otherwise
WORKERS_CI_BRANCH=
# @type=enum(development, preview, production, test)
APP_ENV=remap($WORKERS_CI_BRANCH, "main", production, /.*/, preview, undefined, development)
```

You'll notice that `test` is one of the possible enum values, but it is not listed in the remap.
When running tests, you would just explicitly set `APP_ENV` when invoking your command.

```bash
APP_ENV=test varlock run -- your-test-command
# or if your command is loading varlock internally
APP_ENV=test your-test-command
```

or you could run a production style build locally `APP_ENV=production varlock run -- your-build-command`

:::tip
You can also use the [`forEnv()` helper](/reference/functions/#forenv) to dynamically set whether configuration items are required or optional based on the current environment.
:::

## Setting a _default_ environment flag

You can set the default environment flag directly when running CLI commands using the `--env` flag:

```bash
varlock load --env production
```

This is only useful if you do not want to create a new env var for your env flag, and you are only using varlock via CLI commands.
Mostly it is used internally by some integrations to match existing default behavior, and should not be used otherwise.

:::caution
If `@currentEnv` is used, this will be ignored!
:::

## Using `currentEnv` in Turborepo

Turborepo users should be aware of a common pitfall when using `varlock`'s `@currentEnv` in monorepos managed by Turborepo, especially since Turborepo v2.0+ now enables **Strict Environment Mode** by default.

### The Problem

Turborepo, when running tasks, filters the environment variables available to each task. By default in Strict Mode, **only** variables listed in the `env` or `globalEnv` keys in your `turbo.json` are passed to your scripts. This means that if your environment flag set by `@currentEnv` (e.g., `APP_ENV`) is not explicitly listed, it will not be available to your process, even if you set it in your shell or CI environment. This can cause `varlock` to load the wrong environment, or fail to load the correct `.env.[currentEnv]` file.

### Solution: Add your environment flag to turbo.json

To ensure your environment flag variable is always available to your scripts, add it to the `env` or `globalEnv` section of your `turbo.json`:

```json title="turbo.json"
{
  "globalEnv": ["APP_ENV"],
  "tasks": {
    "build": {
      "env": ["APP_ENV"]
    },
    "dev": {
      "env": ["APP_ENV"]
    }
  }
}
```

- Use `globalEnv` if the variable should be available to all tasks.
- Use `env` under a specific task if only needed for that task.

Now when you run the following:

```bash
APP_ENV=production turbo run build
```

it will load the correct `.env.production` file because the override for `APP_ENV` is passed correctly to `turbo` and in turn to `varlock`.

:::tip
Substitute whatever your environment flag is for `APP_ENV` in the above example. We're _only_ passing `APP_ENV` because that is the variable you are most likely going to want to override in your scripts. If there are other variables you want to pass to your scripts, they will need to be explicitly added as well.
:::

### Setting the Environment Flag

When running locally, or on a platform you control, you can set the env flag explicitly as an environment variable. However on some cloud platforms, there is a lot of magic happening, and the ability to set environment variables per branch is limited. In these cases you can use functions to transform env vars injected by the platform, like a current branch name, into the value you need.

#### Local/Custom Scripts

You can set the env var explicitly when you run a command, but often you will set it in `package.json` scripts:

```diff lang="json" title="package.json" add=/APP_ENV=[a-z]+/
"scripts": {
  "build:preview": "APP_ENV=preview next build",
  "start:preview": "APP_ENV=preview next start",
  "build:prod": "APP_ENV=production next build",
  "start:prod": "APP_ENV=production next start",
  "test": "APP_ENV=test jest"
}
```

#### Vercel

You can use the injected `VERCEL_ENV` variable to match their concept of environment types, while adding your own additional options.

```env-spec title=".env.schema"
# @currentEnv=$APP_ENV
# ---
# @type=enum(development, preview, production)
VERCEL_ENV=
# @type=enum(development, preview, production, test)
APP_ENV=fallback($VERCEL_ENV, development)
```

For more granular environments, use the branch name in `VERCEL_GIT_COMMIT_REF` (see Cloudflare example below).

#### Cloudflare Workers Build

Use the branch name in `WORKERS_CI_BRANCH` to determine the environment:

```env-spec title=".env.schema"
# @currentEnv=$APP_ENV
# ---
WORKERS_CI_BRANCH=
# @type=enum(development, preview, production, test)
APP_ENV=remap($WORKERS_CI_BRANCH, "main", production, /.*/, preview, undefined, development)
```

