> ## 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() configuration reference

> Complete reference for createEnv(), including all config options: server, client, runtimeEnv, adapter, skipValidation, and onValidationError.

`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

```typescript theme={null}
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

<ParamField body="server" type="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.

  ```typescript theme={null}
  server: {
    DATABASE_URL: url(),
    JWT_SECRET:   str().min(32),
    PORT:         port().default(3000),
    NODE_ENV:     str().enum(['development', 'production', 'test']),
    REDIS_URL:    url().optional(),
  }
  ```
</ParamField>

<ParamField body="client" type="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.

  ```typescript theme={null}
  client: {
    NEXT_PUBLIC_API_URL:      url(),
    NEXT_PUBLIC_APP_NAME:     str().default('My App'),
    NEXT_PUBLIC_ENABLE_DEBUG: bool().default(false),
  }
  ```
</ParamField>

<ParamField body="runtimeEnv" type="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.

  ```typescript theme={null}
  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,
  }
  ```

  <Warning>
    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.
  </Warning>
</ParamField>

<ParamField body="adapter" type="'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:

  | Condition                                   | Adapter selected                 |
  | ------------------------------------------- | -------------------------------- |
  | `process` is undefined or has no `version`  | `edge`                           |
  | `process.env.NEXT_RUNTIME` is set           | `nextjs`                         |
  | Any `client` key starts with `NEXT_PUBLIC_` | `nextjs` (with a `console.warn`) |
  | Any `client` key starts with `VITE_`        | `vite` (with a `console.warn`)   |
  | Otherwise                                   | `node`                           |

  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.
</ParamField>

<ParamField body="skipValidation" type="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.

  ```typescript theme={null}
  skipValidation: process.env.NODE_ENV === 'test'
  ```
</ParamField>

<ParamField body="onValidationError" type="(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.

  ```typescript theme={null}
  onValidationError: (err) => {
    myLogger.fatal(err.toJSON())
    process.exit(1)
  }
  ```

  <Warning>
    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.
  </Warning>
</ParamField>

## Return type

`createEnv()` returns a `Readonly`-frozen object. Server variables carry the `ServerOnly<T>` brand; client variables are their plain inferred types.

```typescript theme={null}
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.

<ResponseField name="[server keys]" type="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.
</ResponseField>

<ResponseField name="[client keys]" type="T">
  Each key from the `client` schema returns its plain inferred type - `string`, `number`, `boolean`, or their `| undefined` variants.
</ResponseField>

## Complete usage example

```typescript theme={null}
// 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)
  },
})
```

```typescript theme={null}
// Usage - fully typed, no string | undefined
env.DATABASE_URL          // ServerOnly<string>
env.SMTP_PORT             // ServerOnly<number>
env.NEXT_PUBLIC_APP_NAME  // string
```

<Note>
  `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()`.
</Note>
