Skip to content

Next.js

Varlock is a big upgrade over the default Next.js environment variable tooling, adding validation, type safety, multi-environment management, log redaction, leak detection, and more.

To integrate varlock into a Next.js application, use our @varlock/nextjs-integration package. It provides a drop-in replacement for @next/env, the internal package that handles .env loading, plus a small config plugin that injects our additional security features.

  1. Install varlock and the Next.js integration package

    Terminal window
    npm install @varlock/nextjs-integration varlock
  2. Run varlock init to set up your .env.schema file

    This will guide you through setting up your .env.schema file, based on your existing .env file(s). Make sure to review it carefully.

    Terminal window
    npm exec -- varlock init
  3. Override @next/env with our drop-in replacement

    Next.js does not have APIs we can hook into, so we must override their internal .env-loading package. Overriding dependencies is a bit different for each package manager:

    See NPM overrides docs

    package.json
    {
    "overrides": {
    "next": {
    "@next/env": "npm:@varlock/nextjs-integration"
    }
    }
    }

    Then re-run your package manager’s install command to apply the override:

    Terminal window
    npm install
  4. Enable the Next.js config plugin

    At this point, varlock will now load your .env files into process.env. But to get the full benefits of this integration, you must add varlockNextConfigPlugin to your next.config.* file.

    next.config.ts
    import type { NextConfig } from "next";
    import { varlockNextConfigPlugin } from '@varlock/nextjs-integration/plugin';
    const withVarlock = varlockNextConfigPlugin();
    const nextConfig: NextConfig = {
    // your existing config...
    };
    export default nextConfig;
    export default withVarlock(nextConfig);
  5. Verify the override is active

    Start your dev server (or run a build) and look at the startup output. When the override is working, varlock reports the .env files it loaded with a banner:

    next dev
    - Environments: .env.schema,
    loaded by varlock

    If you don’t see ✨ loaded by varlock ✨, or you get Cannot find module '@next/env' or __VARLOCK_ENV is not set, the override didn’t take effect. See Troubleshooting below.


In a monorepo, the @next/env override from setup step 3 must live at the workspace root so every Next.js app resolves the same replacement. Package-manager overrides apply to all Next.js packages in the workspace, so only add the override once each app you run has its own .env.schema and varlockNextConfigPlugin.


You can continue to use process.env.SOMEVAR as usual, but we recommend using Varlock’s imported ENV object for better type-safety and improved developer experience:

example.ts
import { ENV } from 'varlock/env';
console.log(process.env.SOMEVAR); // 🆗 still works
console.log(ENV.SOMEVAR); // ✨ recommended

To enable type-safety and IntelliSense for your env vars, enable the @generateTsTypes root decorator in your .env.schema. Note that if your schema was created using varlock init, it will include this by default.

.env.schema
# @generateTsTypes(path='env.d.ts')
# ---
# your config items...
  • Non-string values (e.g., number, boolean) are properly typed and coerced
  • All non-sensitive items are replaced at build time (not just NEXT_PUBLIC_)
  • Better error messages for invalid or unavailable keys
  • Enables future DX improvements and tighter control over what is bundled

Varlock can load multiple environment-specific .env files (e.g., .env.development, .env.preview, .env.production).

By default, the environment flag is determined as follows (matching Next.js):

  • test if NODE_ENV is test
  • development if running next dev
  • production otherwise

Instead, we recommend explicitly setting your own environment flag using the @currentEnv root decorator, e.g. APP_ENV. See the environments guide for more information.

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
# @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).

Use the branch name in WORKERS_CI_BRANCH to determine the environment:

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

Next.js uses the NEXT_PUBLIC_ prefix to determine which env vars are public (bundled for the browser). Varlock lets you control this using decorators.

Set @defaultSensitive and mark specific items @sensitive:

.env.schema
# @defaultSensitive=true
# ---
SECRET_FOO= # sensitive by default
# @sensitive=false
NON_SECRET_FOO=

Or, if you’d like to continue using Next.js’s prefix behavior:

.env.schema
# @defaultSensitive=inferFromPrefix('NEXT_PUBLIC_')
# ---
FOO= # sensitive
NEXT_PUBLIC_FOO= # non-sensitive, due to prefix

⚠️ This is only needed if you are using output: standalone

Next’s standalone build command will not copy all our .env files to the .next/standalone directory, so we must copy them manually. Add this to your build command:

package.json
{
"scripts": {
"build": "next build && cp .env.* .next/standalone",
}
}

you may need to adjust if you don’t want to copy certain .local files

Standalone builds do not copy dependency binaries, and varlock depends on the CLI to load. So wherever you are booting your standalone server, you will also need to install the varlock binary and boot your server via varlock run

Terminal window
varlock run -- node .next/standalone/server.js


Almost all setup issues come down to the @next/env override not being applied. The fixes below are ordered by the symptom you see. Work top to bottom.

  • Error: Cannot find module '@next/env' (or no ✨ loaded by varlock ✨ banner)
    💡 The override was recorded in your lockfile but your package manager didn’t materialize it correctly. This is a known package-manager issue with overrides/resolutions for nested scoped packages. npm in particular can create a dangling symlink under node_modules/next/node_modules/@next/env, or skip it entirely, when installing against a stale lockfile.
    • Delete both node_modules and your lockfile, then reinstall. Deleting the lockfile alone is not enough:
      • npm: rm -rf node_modules package-lock.json && npm install
      • pnpm: rm -rf node_modules pnpm-lock.yaml && pnpm install
      • yarn: rm -rf node_modules yarn.lock && yarn install
      • bun: rm -rf node_modules bun.lock bun.lockb && bun install
    • Commit the regenerated lockfile.
    • Re-verify with the banner above, or directly: cat node_modules/@next/env/package.json should show "name": "@varlock/nextjs-integration".
  • process.env.__VARLOCK_ENV is not set
    💡 The override is missing or in the wrong place, so Next loaded its own @next/env instead of ours.
    • Confirm the override config is present and in the correct file for your package manager (see step 3). For pnpm it must be in pnpm-workspace.yaml (v10) or package.json#pnpm.overrides (v9), and in a monorepo it must be at the repo root.
    • Re-run your package manager’s install command, then re-verify the banner.
  • Error [ERR_REQUIRE_ESM]: require() of ES Module ...
    💡 Varlock requires node v22 or higher, which has better CJS/ESM interoperability
  • ❌ Every page 500s in dev with a [turbopack-node]/transforms/transforms.ts ... lint TP1006 error, but only when the project has a middleware.ts file (Next 15.0–15.4 + Turbopack)
    💡 Turbopack on those versions cannot apply JS loaders (which varlock uses) to edge-context files like middleware without breaking dev page rendering. Upgrade to next@^15.5 (where varlock scopes its loader away from edge files automatically) or Next 16 (which fixed the underlying issue), or run next dev without --turbopack.
  • Property 'SOMEVAR' does not exist on type 'TypedEnvSchema'
    💡 If the item does exist in your schema, then the generated types are not being loaded properly by TypeScript