Skip to content

Environments

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 can use functions and 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 how values are set.

While many have traditionally shied away from using environment-specific .env files due to fear of committing sensitive values, the new ability to set values using functions makes it much easier to manage these values in a secure way.

varlock will always treat environment variables passed into the process with the most precedence. This means you can rely on your external hosting platform to inject environment-specific values and still benefit from varlock’s validation and coercion logic.

However we recommend using injected overrides sparingly, and instead moving more config into your .env files.

varlock automatically detects all .env.* files in the current directory. However any environment-specific files (e.g., .env.development) will only be loaded if they match the value of the env var set by the @envFlag root decorator.

The files are applied with a specific precedence (increasing):

  • .env.schema - your schema file, which can also contain default values
  • .env - default values only (not recommended)
  • .env.[envFlag] - environment-specific values
  • .env.local - local overrides (gitignored)
  • .env.[envFlag].local - environment-specific local overrides (gitignored)

For example, consider the following .env.schema:

.env.schema
# @envFlag=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. By default, .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:

Terminal window
APP_ENV=staging varlock run -- node my-test-script.js

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() to transform a value according to a lookup, along with regex() 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.schema
# @envFlag=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, production="main", preview=regex(.*), development=undefined)

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.

Terminal window
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

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

Terminal window
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.

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

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 @envFlag (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.[envFlag] file.

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

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:

Terminal window
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.

Substitute whatever your env flag is for APP_ENV in the above example.


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.

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

package.json
"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"
}

You can use the injected VERCEL_ENV variable to match their concept of environment types:

.env.schema
# @envFlag=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).

Use the branch name in WORKERS_CI_BRANCH to determine the environment:

.env.schema
# @envFlag=APP_ENV
# ---
WORKERS_CI_BRANCH=
# @type=enum(development, preview, production, test)
APP_ENV=remap($WORKERS_CI_BRANCH, production="main", preview=regex(.*), development=undefined)