> ## 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.

# Adapters: Next.js, Node.js, Edge Runtime, and Vite

> next-safe-env adapters handle runtime-specific rules like NEXT_PUBLIC_ enforcement and server var stripping. Adapters are auto-detected or set explicitly.

`next-safe-env` runs in four different execution environments - Next.js App Router, plain Node.js, Vercel Edge Runtime, and Vite - and each one has different rules about which environment variables are accessible. Adapters encapsulate those rules in two hooks: `beforeValidate`, which runs before any field is checked and can throw immediately on schema violations, and `afterValidate`, which runs after all fields pass and can strip keys from the result.

## Auto-detection

When you omit the `adapter` option, `next-safe-env` selects an adapter automatically by inspecting the runtime environment in the following order:

| Condition                                              | Adapter selected                |
| ------------------------------------------------------ | ------------------------------- |
| `typeof process === 'undefined'` or `!process.version` | `edge`                          |
| `process.env.NEXT_RUNTIME` is set                      | `nextjs`                        |
| Any client schema key starts with `VITE_`              | `vite` (with a console warning) |
| Otherwise                                              | `node`                          |

The `NEXT_RUNTIME` variable is set automatically by the Next.js App Router, so you typically get the right adapter without any configuration. The `VITE_` heuristic emits a warning prompting you to set `adapter: 'vite'` explicitly to suppress it.

<Tip>
  Set `adapter` explicitly in every `createEnv()` call to make your intent unambiguous and to silence the auto-detection console warning for Vite projects.
</Tip>

## The `node` adapter

The `node` adapter is designed for Express, Fastify, CLI scripts, and any plain Node.js application that has no client bundle to protect against. Both hooks - `beforeValidate` and `afterValidate` - are no-ops. Raw values go in, validated values come out unchanged. All vars from both the `server` and `client` schemas are present in the returned object.

```typescript theme={null}
import { createEnv, str, url, port, bool } from 'next-safe-env'

export const env = createEnv({
  server: {
    DATABASE_URL: url(),
    PORT:         port().default(3000),
    LOG_LEVEL:    str().enum(['debug', 'info', 'warn', 'error']).default('info'),
    ENABLE_CACHE: bool().default(true),
  },
  client: {},
  runtimeEnv: {
    DATABASE_URL: process.env.DATABASE_URL,
    PORT:         process.env.PORT,
    LOG_LEVEL:    process.env.LOG_LEVEL,
    ENABLE_CACHE: process.env.ENABLE_CACHE,
  },
  adapter: 'node',
})
```

Because there is no browser context in a Node.js application, the `client` schema is typically empty. You can still use it to group vars that you might share with a frontend, but no prefix enforcement is applied.

## The `nextjs` adapter

The `nextjs` adapter targets Next.js App Router and Pages Router applications, where the same module graph is compiled for both the server and the browser.

**`beforeValidate`** iterates every key in the `client` schema and throws synchronously if any key does not start with `NEXT_PUBLIC_`:

```
Error: [next-safe-env] Client env var "API_KEY" must be prefixed with NEXT_PUBLIC_.
```

This is a hard fail-fast. Next.js silently omits client vars that lack the `NEXT_PUBLIC_` prefix from the browser bundle, so a missing prefix would give you an `undefined` value in production with no error message. The adapter makes this mistake impossible.

**`afterValidate`** checks `typeof window !== 'undefined'`. When `true` (browser context), every key that does not start with `NEXT_PUBLIC_` is stripped from the result before the object is returned and frozen. On the server, all keys are returned unchanged.

```typescript theme={null}
import { createEnv, str, url, port, bool } 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']),
  },
  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,
    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',
})
```

<Note>
  Runtime stripping in `afterValidate` is a defense-in-depth measure on top of the TypeScript enforcement. The TypeScript `ServerOnly<T>` brand and `import 'server-only'` are your primary tools. See [Server/Client Split](/concepts/server-client-split) for the full picture.
</Note>

## The `edge` adapter

The Edge Runtime - used for Vercel Edge Runtime and Next.js Middleware (`middleware.ts`) - is neither a full Node.js process nor a browser. It only exposes `NEXT_PUBLIC_` variables at runtime. Server-only secrets are not available.

**`beforeValidate`** does not block validation, but emits a `console.warn` if the `server` schema contains any keys:

```
[next-safe-env] Edge adapter: server vars (API_SIGNING_KEY) will be validated
but stripped from the result - they are not accessible in Edge Runtime.
```

Server vars are still validated - a missing or malformed value still causes a startup failure. They just will not appear in the returned object. The recommended pattern is to leave the `server` schema empty in Edge files.

**`afterValidate`** strips every key that does not start with `NEXT_PUBLIC_` **unconditionally**, regardless of `typeof window`. There is no `window` in the Edge Runtime, so the browser-context check used by the `nextjs` adapter would never trigger.

```typescript theme={null}
import { createEnv, str, url } from 'next-safe-env'

export const env = createEnv({
  server: {},  // prefer empty - server vars are validated but stripped from the result
  client: {
    NEXT_PUBLIC_API_URL:  url(),
    NEXT_PUBLIC_APP_NAME: str().default('My App'),
  },
  runtimeEnv: {
    NEXT_PUBLIC_API_URL:  process.env.NEXT_PUBLIC_API_URL,
    NEXT_PUBLIC_APP_NAME: process.env.NEXT_PUBLIC_APP_NAME,
  },
  adapter: 'edge',
})
```

The key difference between `edge` and `nextjs` is when stripping occurs:

| Adapter  | When server vars are stripped                             |
| -------- | --------------------------------------------------------- |
| `nextjs` | Only in browser context (`typeof window !== 'undefined'`) |
| `edge`   | Always, unconditionally                                   |

## The `vite` adapter

The `vite` adapter is for non-Next.js React applications that use `import.meta.env`. Vite only exposes variables prefixed with `VITE_` to the client bundle, so the adapter enforces that prefix on every client schema key - the same way the `nextjs` adapter enforces `NEXT_PUBLIC_`.

**`beforeValidate`** throws if any `client` schema key does not start with `VITE_`.

**`afterValidate`** strips server vars from the returned object when running in a browser context.

```typescript theme={null}
import { createEnv, str, url, port, bool } from 'next-safe-env'

export const env = createEnv({
  server: {
    DATABASE_URL: url(),
    PORT:         port().default(3000),
  },
  client: {
    VITE_API_URL:  url(),
    VITE_APP_NAME: str().default('My App'),
    VITE_DEBUG:    bool().default(false),
  },
  runtimeEnv: {
    DATABASE_URL:  process.env.DATABASE_URL,
    PORT:          process.env.PORT,
    VITE_API_URL:  import.meta.env.VITE_API_URL,
    VITE_APP_NAME: import.meta.env.VITE_APP_NAME,
    VITE_DEBUG:    import.meta.env.VITE_DEBUG,
  },
  adapter: 'vite',
})
```

<Note>
  In Vite projects, use `import.meta.env.VITE_*` for client vars in `runtimeEnv`, not `process.env`. Server vars that are only available during the build step (such as from a `.env` file loaded by Vite's server process) can still use `process.env`.
</Note>

The `vite` adapter is auto-detected when any client schema key starts with `VITE_`, but it emits a console warning asking you to set `adapter: 'vite'` explicitly. Set it explicitly to suppress the warning and make your configuration self-documenting.

## When to set `adapter` explicitly

Auto-detection covers the common cases, but there are situations where you should always set `adapter` explicitly:

* **Vite projects** - auto-detection emits a warning; always set `adapter: 'vite'`.
* **Next.js Pages Router** - `NEXT_RUNTIME` may not be set during the initial page render in older Next.js versions; set `adapter: 'nextjs'` to be safe.
* **Monorepos with shared env files** - if `env.ts` is imported from both a Next.js app and a Node.js script, each entry point should call `createEnv()` independently with the correct `adapter` set explicitly.
* **Any environment where runtime detection is ambiguous** - explicit configuration is always more reliable than heuristics.
