env

Inlines fixed string values into the bundle at build time via webpack DefinePlugin substitution.

Signature
env?: Record<string, string>
  • Deprecated since PHP Legacy API since v9.4.0, superseded by .env files; still supported for backward compatibility.

Parameters

ParameterTypeDescription
$envoptionalRecord<string, string>= undefinedKeys become process.env.<key> references that are textually replaced with the literal value during the build. Values are always included in the JavaScript bundle, on the server and in the browser, regardless of whether the key carries a NEXT_PUBLIC_ prefix.

Changelog

  • PHP v9.4.0.env file support added; env in next.config became the legacy path and is no longer recommended.

This is a legacy API. It predates .env file support, still works, and for almost every case the .env files with a NEXT_PUBLIC_ prefix are the better answer. Reach for it only when a value has to be computed inside next.config.ts itself — read from a JSON file, derived from the git SHA, chosen by build phase.

What trips people up

Everything listed here is shipped to the browser. The NEXT_PUBLIC_ prefix means nothing in this object: it only controls exposure for values coming from the environment or .env files. Put an API secret in env and it is in your client bundle, readable by anyone.

The second trap is that this is textual substitution by webpack's DefinePlugin, not a real object. process.env.customKey is rewritten to 'my-value' before the code runs, so const { customKey } = process.env yields undefined — there is no property to destructure. Dynamic access such as process.env[name] fails for the same reason. Always write the full static member expression.

Example

Example · typescript
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  env: {
    customKey: 'my-value',
  },
}

export default nextConfig

When to use it

  • Expose a build identifier such as the git SHA to client code
  • Derive a public value inside next.config.ts that a .env file cannot compute
  • Keep a legacy configuration working while migrating to .env files

Frequently asked questions

Should I use env or a .env file?

A .env file, in almost every case. env in next.config is a legacy API kept for backward compatibility since Next.js 9.4. Use env only for values that must be computed by code inside next.config itself.

Does NEXT_PUBLIC_ keep a value in env private?

No. The prefix only has an effect for variables supplied through the environment or .env files. Everything in the env object is always inlined into the JavaScript bundle and visible in the browser.

Why is destructuring process.env returning undefined?

Because webpack's DefinePlugin performs a textual replacement of the exact expression process.env.customKey. Destructuring or dynamic indexing leaves no such expression to substitute, so nothing is replaced and the lookup finds nothing at runtime.

More examples

Reading the value, and the destructuring trap

Only the literal member expression process.env.customKey is replaced. The compiled output for the working line is {'my-value'} — the lookup never happens at runtime. Both other forms compile untouched and read a process.env that has no such key in the browser, which is why the value appears to vanish only after a build.

Example · typescript
// app/page.tsx
export default function Page() {
  // works: rewritten to {'my-value'} at build time
  const ok = process.env.customKey

  // undefined: DefinePlugin substitutes the full expression,
  // it does not create a real property on process.env
  const { customKey } = process.env

  // undefined for the same reason: no static expression to replace
  const name = 'customKey'
  const alsoBroken = process.env[name]

  return <h1>The value of customKey is: {ok}</h1>
}

Computing a value in the config

This is the case env is still worth using for: a value that has to be produced by running code at build time, which a static .env file cannot express. next.config.ts runs in Node during the build, so child_process and the filesystem are available. The result is baked into the client bundle, so restrict it to values that are safe to publish.

Example · typescript
import { execSync } from 'node:child_process'
import type { NextConfig } from 'next'

const commit = execSync('git rev-parse --short HEAD').toString().trim()

const nextConfig: NextConfig = {
  env: {
    // safe to expose: identifies the build, not a credential
    buildCommit: commit,
  },
}

export default nextConfig

Discussion

  • Be the first to comment on this lesson.