Skip to main content

Documentation Index

Fetch the complete documentation index at: https://next-safe-env.dev/llms.txt

Use this file to discover all available pages before exploring further.

createEnv() is the single entry point for next-safe-env. You call it once - typically in a dedicated src/env.ts file - and it validates every environment variable in your schema against runtimeEnv before your application serves a single request. If any variable is missing or malformed, it prints every problem at once and exits with a non-zero code. When all variables pass, it returns a fully-typed, frozen object.

Function signature

function createEnv<
  TServer extends Schema | ZodObjectLike<Record<string, unknown>>,
  TClient extends Schema | ZodObjectLike<Record<string, unknown>>,
>(config: EnvConfig<TServer, TClient>): InferEnv<TServer, TClient>

Config options

server
Schema | ZodObjectLike
required
The schema for your server-only environment variables. Each key maps to a validator built with str(), num(), bool(), url(), or port(), or to a Zod schema when using Zod interop.Variables defined here are branded as ServerOnly<T> in the returned object. They are never included in client bundles when the nextjs or edge adapter is active.
server: {
  DATABASE_URL: url(),
  JWT_SECRET:   str().min(32),
  PORT:         port().default(3000),
  NODE_ENV:     str().enum(['development', 'production', 'test']),
  REDIS_URL:    url().optional(),
}
client
Schema | ZodObjectLike
required
The schema for environment variables that are safe to expose to the client bundle. Each key follows the same validator syntax as server.When the nextjs adapter is active (including auto-detected), every key in client must be prefixed with NEXT_PUBLIC_. When the vite adapter is active, every key must be prefixed with VITE_. The adapter throws immediately in beforeValidate if a key violates this rule.
client: {
  NEXT_PUBLIC_API_URL:      url(),
  NEXT_PUBLIC_APP_NAME:     str().default('My App'),
  NEXT_PUBLIC_ENABLE_DEBUG: bool().default(false),
}
runtimeEnv
Record<string, string | undefined>
required
An explicit map of every key in server and client to its runtime value. You must list each key individually - do not spread the whole process.env object.Bundlers like Next.js and Vite inline each process.env.VAR reference statically at build time. Passing the entire process.env object prevents this inlining, defeats tree-shaking, and can leak server variables into the client bundle.
runtimeEnv: {
  DATABASE_URL:             process.env.DATABASE_URL,
  JWT_SECRET:               process.env.JWT_SECRET,
  PORT:                     process.env.PORT,
  NODE_ENV:                 process.env.NODE_ENV,
  REDIS_URL:                process.env.REDIS_URL,
  NEXT_PUBLIC_API_URL:      process.env.NEXT_PUBLIC_API_URL,
  NEXT_PUBLIC_APP_NAME:     process.env.NEXT_PUBLIC_APP_NAME,
  NEXT_PUBLIC_ENABLE_DEBUG: process.env.NEXT_PUBLIC_ENABLE_DEBUG,
}
A key present in server or client but absent from runtimeEnv resolves to undefined at validation time. If the validator for that key is not .optional() and has no .default(), validation fails.
adapter
'nextjs' | 'node' | 'edge' | 'vite'
The adapter that controls prefix enforcement and runtime key-stripping. When omitted, createEnv() auto-detects the adapter using the following order:
ConditionAdapter selected
process is undefined or has no versionedge
process.env.NEXT_RUNTIME is setnextjs
Any client key starts with NEXT_PUBLIC_nextjs (with a console.warn)
Any client key starts with VITE_vite (with a console.warn)
Otherwisenode
Pass adapter explicitly to suppress the Pages Router and Vite detection warnings.
  • nextjs - enforces NEXT_PUBLIC_ prefix on all client keys; strips server vars from the result in browser context.
  • node - no prefix rules, no key-stripping. Use for Express, Fastify, CLI scripts, and plain Node.js apps.
  • edge - strips all non-NEXT_PUBLIC_ keys from the result unconditionally. Server vars are still validated before being stripped.
  • vite - enforces VITE_ prefix on all client keys; strips server vars from the result in browser context.
skipValidation
boolean
default:"false"
When true, field-level validation and beforeValidate are skipped entirely. The afterValidate hook still runs, so adapter key-stripping still applies. Raw string values from runtimeEnv are returned in the typed shape.Use this in test environments where some variables are intentionally absent. Never set it to true in production or in CI validation steps.
skipValidation: process.env.NODE_ENV === 'test'
onValidationError
(error: ValidationErrorShape) => never
A custom handler called instead of the default console.error(err.format()) + process.exit(1) when one or more variables fail validation.The handler receives a ValidationErrorShape (satisfied by EnvValidationError) and must never return. If it returns, execution continues with an invalid env object.
onValidationError: (err) => {
  myLogger.fatal(err.toJSON())
  process.exit(1)
}
The onValidationError handler must never return. Call process.exit(), throw an error, or otherwise terminate execution inside it. createEnv() does not call process.exit after invoking this handler.

Return type

createEnv() returns a Readonly-frozen object. Server variables carry the ServerOnly<T> brand; client variables are their plain inferred types.
type InferEnv<TServer, TClient> = Readonly<
  { [K in keyof InferInput<TServer>]: ServerOnly<InferInput<TServer>[K]> } &
    InferInput<TClient>
>
The object is frozen with Object.freeze() at runtime. Mutation will silently fail in non-strict mode and throw a TypeError in strict mode.
[server keys]
ServerOnly<T>
Each key from the server schema returns its inferred type branded as ServerOnly<T>. The brand is T & { readonly [SERVER_ONLY_BRAND]: void } - assignment-compatible with T in TypeScript’s structural type system, and visible in IDE tooltips.For a hard compile error when client components import server vars, add import 'server-only' at the top of your env.ts file.
[client keys]
T
Each key from the client schema returns its plain inferred type - string, number, boolean, or their | undefined variants.

Complete usage example

// src/env.ts
import { createEnv, str, num, bool, url, port } from 'next-safe-env'

export const env = createEnv({
  server: {
    DATABASE_URL: url(),
    JWT_SECRET:   str().min(32),
    SMTP_PORT:    port().default(587),
    NODE_ENV:     str().enum(['development', 'production', 'test']),
    REDIS_URL:    url().optional(),
  },
  client: {
    NEXT_PUBLIC_API_URL:      url(),
    NEXT_PUBLIC_APP_NAME:     str().default('My App'),
    NEXT_PUBLIC_ENABLE_DEBUG: bool().default(false),
  },
  runtimeEnv: {
    DATABASE_URL:             process.env.DATABASE_URL,
    JWT_SECRET:               process.env.JWT_SECRET,
    SMTP_PORT:                process.env.SMTP_PORT,
    NODE_ENV:                 process.env.NODE_ENV,
    REDIS_URL:                process.env.REDIS_URL,
    NEXT_PUBLIC_API_URL:      process.env.NEXT_PUBLIC_API_URL,
    NEXT_PUBLIC_APP_NAME:     process.env.NEXT_PUBLIC_APP_NAME,
    NEXT_PUBLIC_ENABLE_DEBUG: process.env.NEXT_PUBLIC_ENABLE_DEBUG,
  },
  adapter: 'nextjs',
  skipValidation: process.env.NODE_ENV === 'test',
  onValidationError: (err) => {
    console.error(err.format())
    process.exit(1)
  },
})
// Usage - fully typed, no string | undefined
env.DATABASE_URL          // ServerOnly<string>
env.SMTP_PORT             // ServerOnly<number>
env.NEXT_PUBLIC_APP_NAME  // string
createEnv() does not load .env files. It validates what is already in process.env. Use Next.js’s built-in .env support or dotenv to load variables before calling createEnv().