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.

next-safe-env gives you a single env object where every variable is validated at startup, typed precisely, and automatically split between server and client bundles. Instead of scattering process.env.X calls across your codebase and discovering missing values mid-request, you define your schema once in src/env.ts and get full TypeScript autocomplete everywhere you import it.
1

Install next-safe-env

Add the package to your project. It has zero runtime dependencies.
npm install next-safe-env
2

Create src/env.ts

Define your server and client schemas. Pass each process.env reference individually in runtimeEnv - this lets the Next.js bundler inline each value statically and prevents server vars from leaking into the client bundle.
// src/env.ts
import { createEnv, str, 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),
  },
  // Pass individual process.env.VAR references, not the whole process.env object.
  // This lets the bundler inline each value statically, which prevents server
  // vars from being included in the client bundle.
  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,
  },
})
next-safe-env validates what is already in process.env. It does not load .env files. Next.js loads .env files automatically, so no extra setup is needed.
3

Use it in a server context

Import env in any server-side file - API routes, Server Components, server actions. All vars from both schemas are available here.
// app/api/auth/route.ts - server context, all vars available
import { env } from '@/env'

export async function POST() {
  const db   = await connectDB(env.DATABASE_URL)  // string
  const port = env.SMTP_PORT                       // number, not string
  const debug = env.NEXT_PUBLIC_ENABLE_DEBUG       // boolean
}
4

Use it in a client component

Import the same env object in Client Components. Client vars are fully typed and autocomplete. Server vars carry the ServerOnly<T> brand, which is visible in IDE tooltips.
// app/components/Header.tsx - client component
import { env } from '@/env'

export function Header() {
  // ✅ Client vars are fully typed and autocomplete
  return <h1>{env.NEXT_PUBLIC_APP_NAME}</h1>

  // ⚠️  Server vars carry the ServerOnly<T> brand - visible in IDE tooltips.
  // For a hard compile error that prevents client components from reading
  // server vars, add `import 'server-only'` at the top of your env.ts file.
}
The ServerOnly<T> brand is visible in IDE tooltips and communicates a variable’s origin at a glance, but it does not produce a TypeScript compile error on its own due to structural typing. To get a hard build-time error when env.ts is imported in a client bundle, add import 'server-only' at the top of src/env.ts. Next.js will then throw at build time if the module is included in any client bundle.

NEXT_PUBLIC_ prefix enforcement

The nextjs adapter checks every key in your client schema before any validation runs. If a key is missing the NEXT_PUBLIC_ prefix, createEnv throws immediately:
Error: [next-safe-env] Client env var "API_KEY" must be prefixed with NEXT_PUBLIC_.
This is a hard fail-fast. Without the prefix, Next.js silently omits the variable from the browser bundle and you get undefined at runtime with no warning. next-safe-env surfaces the mistake at startup instead.

Browser context stripping

When your code runs in a browser context (typeof window !== 'undefined'), the nextjs adapter strips every non-NEXT_PUBLIC_ key from the returned env object before it reaches your component. This is a runtime safety net layered on top of the TypeScript enforcement - even if a server var somehow ends up referenced in a client bundle, its value will be undefined rather than the secret it holds. On the server side, all vars from both schemas are returned unchanged.

Adapter auto-detection

You do not need to set adapter: 'nextjs' explicitly in most cases. next-safe-env auto-detects the correct adapter using this order:
ConditionAdapter selected
process is undefined or has no versionedge
process.env.NEXT_RUNTIME is setnextjs
Any client key starts with NEXT_PUBLIC_nextjs + console.warn
Otherwisenode
Next.js App Router sets NEXT_RUNTIME automatically, so detection is reliable. Set adapter: 'nextjs' explicitly if you want to suppress the Pages Router heuristic warning or prefer predictable, self-documenting config.

What happens when validation fails

If any variable is missing or invalid, the app refuses to start and prints every problem at once. You never see just the first failure.
[next-safe-env] Environment validation failed - 3 error(s):

  ✗ DATABASE_URL     - Required. Expected a valid URL. Got: "postgres-localhost"
  ✗ JWT_SECRET       - Too short. Must be ≥ 32 characters. Got length: 12
  ✗ SMTP_PORT        - Invalid port. Must be 1–65535. Got: "99999"

  Server vars:  2 valid, 3 invalid
  Client vars:  2 valid, 0 invalid

  Set the correct values in your .env file or deployment environment and restart.
Fix all reported values in your .env file or deployment environment and restart. The full list is always shown before exit.