---
title: Next.js
description: How to integrate Varlock with Next.js for secure, type-safe environment management
---
import SyncedCodeTabs from '@/components/SyncedCodeTabs.astro'
import { TabItem, Tabs, Steps } from "@astrojs/starlight/components";
import Badge from '@/components/Badge.astro';
import ExecCommandWidget from '@/components/ExecCommandWidget.astro';
import InstallJsDepsWidget from '@/components/InstallJsDepsWidget.astro';

<div class="page-badges">
  <Badge npmPackage="@varlock/nextjs-integration" />
</div>

Varlock is a big upgrade over the [default Next.js environment variable tooling](https://nextjs.org/docs/pages/guides/environment-variables), 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`](https://npmx.dev/package/@varlock/nextjs-integration) package. It provides a drop-in replacement for [`@next/env`](https://www.npmjs.com/package/@next/env), the internal package that handles .env loading, plus a small config plugin that injects our additional security features.

## Setup

:::caution[Requirements]
- Node.js v22 or higher
- Next.js v14 or higher (v15 and v16 fully supported, including Turbopack)
:::

<Steps>

1. **Install varlock and the Next.js integration package**
    <InstallJsDepsWidget packages="@varlock/nextjs-integration varlock" />

1. **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.

    <ExecCommandWidget command="varlock init" showBinary={false} />

1. **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:

    <Tabs syncKey='package-manager'>
      <TabItem label="npm">
        See [NPM overrides docs](https://docs.npmjs.com/cli/v9/configuring-npm/package-json#overrides)

        ```diff lang="json" title="package.json"
        {
        +  "overrides": {
        +    "next": {
        +      "@next/env": "npm:@varlock/nextjs-integration"
        +    }
        +  }
        }
        ```

        :::caution[npm may not apply the override on a plain `npm install`]
        npm can be inconsistent about applying `overrides` for nested scoped packages like `@next/env`, especially when an existing `package-lock.json` has it pinned to a stale location. If after installing you hit `Cannot find module '@next/env'` or don't see the varlock banner, delete both `node_modules` and `package-lock.json` and reinstall. See [Troubleshooting](#troubleshooting).
        :::
      </TabItem>

      <TabItem label="yarn">
        See [yarn resolutions docs](https://yarnpkg.com/configuration/manifest#resolutions)

        ```diff lang="json" title="root/package.json"
        {
        +  "resolutions": {
        +    "**/@next/env": "npm:@varlock/nextjs-integration"
        +  },
        }
        ```
        **In a monorepo, this override must be done in the monorepo's root package.json file!**
      </TabItem>

      <TabItem label="pnpm">
        <Tabs syncKey='pnpm-version'>
          <TabItem label="pnpm version 10+">
            See [pnpm v10 overrides docs](https://pnpm.io/settings#overrides)

            ```diff lang="yaml" title="root/pnpm-workspace.yaml"
            packages: # <- ⚠️ this field is also required
              - .     # set this to '.' if not in a monorepo
            +overrides:
            +  "@next/env": "npm:@varlock/nextjs-integration"
            ```
            **This must be set in `pnpm-workspace.yaml`, which lives at the root of your repo, regardless of whether you are using a monorepo or not.**
          </TabItem>
          <TabItem label="pnpm version 9">
            See [pnpm v9 overrides docs](https://pnpm.io/9.x/package_json#pnpmoverrides)

            ```diff lang="json" title="root/package.json"
            {
            +  "pnpm": {
            +    "overrides": {
            +      "@next/env": "npm:@varlock/nextjs-integration"
            +    }
            +  }
            }
            ```
            **In a monorepo, this override must be done in the monorepo's root package.json file!**
          </TabItem>
        </Tabs>
      </TabItem>
      <TabItem label="bun">
        See [Bun overrides docs](https://bun.com/docs/pm/overrides)

        ```diff lang="json" title="package.json"
        {
        +  "overrides": {
        +    "@next/env": "npm:@varlock/nextjs-integration"
        +  }
        }
        ```
      </TabItem>
    </Tabs>

    Then re-run your package manager's install command to apply the override:
    <SyncedCodeTabs
      group="package-manager"
      tabs={[
        {
          label: "npm",
          code: "npm install",
          lang: "bash",
        },
        {
          label: "yarn",
          code: "yarn install",
          lang: "bash",
        },
        {
          label: "pnpm",
          code: "pnpm install",
          lang: "bash",
        },
        {
          label: "bun",
          code: "bun install",
          lang: "bash",
        },
      ]}
    />

1. **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.

    ```diff lang="ts" title="next.config.ts" "varlockNextConfigPlugin"
    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);
    ```

1. **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:

    ```bash title="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](#troubleshooting) below.

    :::note[The override only needs to apply at build/dev time]
    On serverless platforms (e.g. Vercel, Cloudflare), the fully-resolved env is baked into your build output by the config plugin, so the production server does not re-load `.env` files. This means you only need the `@next/env` override to be active when you run `next dev` and `next build`. There is no separate runtime config to set on the platform.
    :::

</Steps>

---

## Monorepos

In a [monorepo](/guides/monorepos/), 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`.

---

## Accessing environment variables

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:

```ts title="example.ts"
import { ENV } from 'varlock/env';

console.log(process.env.SOMEVAR); // 🆗 still works
console.log(ENV.SOMEVAR);         // ✨ recommended
```

:::caution
If you are not using the `varlockNextConfigPlugin`, only `process.env` will work.
:::

### Type-safety and IntelliSense

To enable type-safety and IntelliSense for your env vars, enable the [`@generateTsTypes` root decorator](/reference/root-decorators/#generatetstypes) in your `.env.schema`. Note that if your schema was created using `varlock init`, it will include this by default.

```diff lang="env-spec" title=".env.schema"
+# @generateTsTypes(path='env.d.ts')
# ---
# your config items...
```

#### Why use `ENV` instead of `process.env`?
- 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

---

## Managing multiple environments

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

:::tip
Without a custom env flag, you cannot use non-production env files (like `.env.preview`, `.env.staging`) for non-prod deployments.
:::

Instead, we recommend explicitly setting your own environment flag using the [`@currentEnv` root decorator](/reference/root-decorators/#currentenv), e.g. `APP_ENV`. See the [environments guide](/guides/environments) for more information.

:::note[Loading `.env.local` in `test` environment]
Next.js makes [a special exception](https://nextjs.org/docs/pages/guides/environment-variables#test-environment-variables) to skip loading `.env.local` if the current environment is `test`.

Varlock does not (following Vite's lead), but you may explicitly recreate that behavior:

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

:::note[Precedence of `.env.local` vs `.env.[currentEnv]`]
Next.js swaps the order of precedence for `.env.local` vs `.env.[currentEnv]` compared to Varlock.
:::

### 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:

```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,
  development
)
```

---

## Managing sensitive config values

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`](/reference/root-decorators/#defaultsensitive) and mark specific items [`@sensitive`](/reference/item-decorators/#sensitive):

```diff lang="env-spec" title=".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:

```diff lang="env-spec" title=".env.schema"
+# @defaultSensitive=inferFromPrefix('NEXT_PUBLIC_')
# ---
FOO= # sensitive
NEXT_PUBLIC_FOO= # non-sensitive, due to prefix
```


:::caution[Bundling behavior]
All non-sensitive items are bundled at build time via `ENV`, while `process.env` replacements only include `NEXT_PUBLIC_`-prefixed items.
:::


## Extra setup for standalone mode ||standalone||

**⚠️ 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:
```diff lang="json" title="package.json" add=/ && [^"]+/
{
  "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](/getting-started/installation/) and boot your server via [`varlock run`](/reference/cli-commands/#run)

```bash
varlock run -- node .next/standalone/server.js
```


---

:::note[Encrypting the env blob]
When deploying to serverless platforms like Vercel, Varlock injects the fully resolved env data into your server-side build output. By default, this blob is plaintext JSON, meaning anyone with access to the build artifact can read your secrets. You can encrypt it for extra protection (e.g., against sourcemap leaks). See the [encrypted deployments guide](/guides/encrypted-deployments/) for setup instructions.
:::

---
## Troubleshooting

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)
  <br/>💡 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`
  <br/>💡 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](#setup)). 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 ...`
  <br/>💡 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)
  <br/>💡 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'`
  <br/>💡 If the item does exist in your schema, then the generated types are not being loaded properly by TypeScript
  - make sure the [`@generateTsTypes` root decorator](/reference/root-decorators/#generatetstypes) is enabled
  - ensure the path to the generated types file is included in your `tsconfig.json`


---

## Reference
- [Root decorators reference](/reference/root-decorators)
- [Item decorators reference](/reference/item-decorators)
- [Functions reference](/reference/functions)
- [Next.js environment variable docs](https://nextjs.org/docs/pages/guides/environment-variables)
