[]previous and next page

Public environment variables (env.public)

Code that is shared between the browser and the server reads process.env.NEXT_PUBLIC_SITE_URL (Next's convention) or process.env.DUSK_PUBLIC_X.

For agentsUpdated

Code that is shared between the browser and the server reads process.env.NEXT_PUBLIC_SITE_URL (Next's convention) or process.env.DUSK_PUBLIC_X. Set the prefixes in dusk.config.ts:

ts
export default defineConfig({
  env: { public: { prefixes: ["NEXT_PUBLIC_"], processEnv: true } },
});
  • prefixes lists the variables that reach the browser, in addition to DUSK_PUBLIC_ and Vite's envPrefix. Each prefix is at least three characters; * is refused. The same names are then also import.meta.env.NAME in client code.
  • processEnv: true gives the client bundle process.env. Every public variable a file reads as process.env.NAME is replaced by its value at build time, and a process.env object holding the public variables only is defined for dynamic reads. The server keeps the real process.env; no secret is ever defined in the browser.
  • The key public is reserved in env: it is the public contract, not a server variable, so DATABASE_URL and the other declarations of env are unchanged.
  • A public name can also come from .env.public (committed; see Env files while you develop), whatever its prefix. A name declared in .env.public and in a server file fails with [ENV_PUBLIC_CONFLICT].
  • The build checks what the browser gets. A public value that looks like a credential (a sk_live_ or sk_test_ key, a private key block, a password inside a URL, a JWT) fails with [ENV_PUBLIC_VALUE]; the message names the variable and its file, never the value. A public name that reads like a credential (a name that contains SECRET, TOKEN, PASSWORD, PASS, KEY, DATABASE_URL, DSN, PRIVATE or CREDENTIAL, for example DUSK_PUBLIC_API_TOKEN) only gets a warning, [ENV_PUBLIC_NAME], which names the variable and never its value. A publishable key has such a name legitimately, so the name alone never fails the build; the value check above decides. List the name in allow to silence the warning. A server variable that looks like a credential (SECRET, TOKEN, KEY, DATABASE, COOKIE ...) whose value appears in a client file fails with [ENV_SECRET_EXPOSED], naming the variable and the file.
  • allow lists public names whose values are public on purpose (a vendor's publishable key, a preview link with a token). Their values are not checked by [ENV_PUBLIC_VALUE], their names get no [ENV_PUBLIC_NAME] warning, and they are not treated as a leak in the client output. Each name is a string: allow: ["DUSK_PUBLIC_STRIPE_PUBLISHABLE_KEY"].
  • A client read that the build cannot check fails with [ENV_NOT_PUBLIC]. Checked forms: import.meta.env.NAME, import.meta.env["NAME"], a destructuring of import.meta.env or process.env (const { NAME } = import.meta.env), a variable that holds one of them (const e = import.meta.env; e.NAME), and process.env.NAME or process.env["NAME"] for a name that is not public. process.env.NODE_ENV is allowed. A key that is not a string literal, such as import.meta.env[key], cannot be checked, so it fails too, with a message that says so: read the public name directly.
  • A variable with the Next.js prefix, NEXT_PUBLIC_, is server-only unless NEXT_PUBLIC_ is in env.public.prefixes (or Vite's envPrefix). Dusk does not read that prefix as public by default, so such a name never reaches the browser and nothing fails. When the app lists the prefix, the dev server prints one [ENV_NEXT_PUBLIC] note recommending the Dusk prefix DUSK_PUBLIC_. Renaming is the recommended path; keeping the prefix is supported.
  • In mode test, .env.local is not read, as Next.js does not read it in tests; .env, .env.test and .env.test.local are. Put a value that a test needs in .env.test, not in a file a developer keeps local.
  • In dusk dev the client entry loads a small shim first, so process.env works the same way; it holds the public variables only.
  • A field of defineEnv marked secret: true cannot have a public prefix: DUSK_PUBLIC_ always counts, and so does a prefix passed as defineEnv(shape, { publicPrefixes: ["NEXT_PUBLIC_"] }). Such a schema fails with [ENV_SECRET_PUBLIC] when its module loads. The build does not evaluate the server schema, so this check is at server start (or wherever the module is first imported), not in dusk build. parsed.public holds the public names that are not secret.

Typings. src/env.public.d.ts is generated (names only) and rewritten by dusk dev and dusk build. It types each public name twice: import.meta.env.NAME is readonly NAME: string (the build inlines it, so an assignment has no effect), and process.env.NAME takes Node's own type, an optional, writable string (NAME?: string). A test or a script may therefore assign or delete a public name in process.env, as the runtime allows.

Without processEnv nothing changes: import.meta.env.DUSK_PUBLIC_* remains the browser's way to read a variable.

Each process.env.NAME of a public name in client code is replaced by its value in every chunk: the literal is inlined. Vite would otherwise replace a bare process.env with an empty object in the browser, so a read of a public name in a chunk that loads before any runtime shim was undefined. No shim is needed, so the order of the entry's imports does not matter. processEnv: true also defines the whole process.env object of the public variables. A browser shim that copies import.meta.env into process.env can be deleted: import.meta.env is already inlined, and process.env reads are too.