Next.js adapter for envra: split server vs client env schemas, enforce NEXT_PUBLIC_ on client keys, and block secrets on the client block.
pnpm add @envra/next @envra/core
# or
npm install @envra/next @envra/corePeers: next >= 14 (optional for typing in non-Next contexts), @envra/core ^0.1.2 (install explicitly so a single copy is used — avoids duplicate FieldBuilder types in TypeScript).
import { defineNextEnv, str, url, secret } from "@envra/next";
export const env = defineNextEnv({
server: {
DB_URL: secret(url()),
},
client: {
NEXT_PUBLIC_APP_URL: url(),
},
runtimeEnv: process.env,
});Use when you only validate NEXT_PUBLIC_* keys and want a module that is safe to import from Client Components (no server secrets in that file):
import { defineNextPublicEnv, url } from "@envra/next";
/** Each key must use `process.env.NEXT_PUBLIC_*` directly — see “Client bundles” below. */
const nextPublicRuntimeEnv = {
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
};
export const publicEnv = defineNextPublicEnv({
client: {
NEXT_PUBLIC_APP_URL: url(),
},
runtimeEnv: nextPublicRuntimeEnv,
});For secrets and server-only variables, keep a separate module with import "server-only" and defineNextEnv (or defineEnv from @envra/core).
Next.js (webpack / Turbopack) inlines NEXT_PUBLIC_* into the browser bundle only when the code contains static access such as process.env.NEXT_PUBLIC_APP_URL.
If you pass process.env as a whole to defineNextPublicEnv (or any helper that reads keys dynamically), the bundler cannot inject those values for the client. At runtime, publicEnv may look correct on the server but be empty or wrong in the browser, even when .env / .env.local are set.
Do this for client-imported env modules:
- Build
runtimeEnvas an object with one explicit property perNEXT_PUBLIC_*key your schema uses (as in the example above), or - Read critical values with direct
process.env.NEXT_PUBLIC_*in small modules (e.g. a single API base URL helper) that client code imports.
runtimeEnv: process.env remains fine for server-only modules (no Client Component import chain), because Node’s process.env is complete at runtime.
See also: Next.js — Bundling Environment Variables for the Browser.
- Server env — e.g.
lib/env.tswithimport "server-only"at the top, thendefineNextEnvwith bothserverandclient(or server-only schema via@envra/core). - Public env — e.g.
lib/env-public.tswithoutserver-only, usingdefineNextPublicEnvor only theclientblock patterns above. - Do not import the server env module from code that is bundled for the client (Client Components, or shared
services//lib/pulled in by them). UsepublicEnvfor anything that needs env inside client bundles.
If TypeScript reports that FieldBuilder types are incompatible (separate declarations of a private property), you likely have two versions of @envra/core installed. Fix with a single version, for example:
{
"pnpm": {
"overrides": {
"@envra/core": "0.1.2"
}
}
}@envra/next re-exports common builders (str, int, secret, …), FieldBuilder, InferSchema, and defineEnv from @envra/core so you can use one import path in Next apps:
import { defineNextEnv, str, type InferSchema } from "@envra/next";MIT — see LICENSE in the monorepo.