When you build a Next.js application, the same JavaScript module system serves both the server and the browser - which means a secret likeDocumentation 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.
DATABASE_URL can end up in the client bundle if nothing prevents it. next-safe-env closes that gap with two complementary layers: TypeScript branding that makes server vars visible in your IDE, and runtime stripping that removes them from the object before the browser ever sees it.
The server and client schema keys
Every call to createEnv() requires two top-level schema keys: server and client. Splitting the schema at this level is intentional - the separation signals intent to the reader, to TypeScript, and to the adapter layer.
server is treated as a secret that must never reach the browser. Anything in client is explicitly safe to expose. The adapter layer reads this distinction and uses it to enforce prefix rules and strip vars at runtime.
The ServerOnly<T> brand
Every key in the server schema is returned with the ServerOnly<T> brand applied:
- Your IDE tooltips show the type as
ServerOnly<string>instead of plainstring, so the origin of a value is always visible at a glance. - Because
ServerOnly<T>extendsT, it remains assignment-compatible withTat the TypeScript level. Passingenv.DATABASE_URLwhere astringis expected will not produce a compile error on its own.
Getting a hard compile error with import 'server-only'
To make TypeScript throw a build-time error if your env.ts is ever imported in a client bundle, add import 'server-only' at the top of the file:
server-only package causes a build error the moment the module is included in a client bundle. Combined with the ServerOnly<T> brand, this gives you two layers of compile-time protection.
server-only is a Next.js package. Install it with npm install server-only and make sure you are on Next.js 13 or later. For plain Node.js projects this import is unnecessary - there is no client bundle.The ClientEnv<T> utility type
ServerOnly<T> makes server vars visible but doesn’t block access to them by key name. If you want a type that excludes server vars entirely - so that a function accepting it cannot even reference a server key - use the ClientEnv<T> utility type:
ClientEnv<T> uses a mapped type with a key filter: any property whose type extends { readonly [SERVER_ONLY_BRAND]: void } is dropped from the resulting type. The function signature (vars: ClientVars) then acts as a compile-time boundary - TypeScript will error if you try to pass a server-keyed value into it.
Runtime stripping: defense-in-depth
TypeScript enforcement disappears at runtime. The adapter layer provides a second line of defense by physically removing server var keys from the returned object when running in a browser context. For thenextjs adapter, afterValidate checks typeof window !== 'undefined'. If true (browser context), every non-NEXT_PUBLIC_ key is deleted from the result before it is returned and frozen. Server-side, all keys are returned unchanged.
For the edge adapter, stripping is unconditional - server vars are always removed because the Edge Runtime exposes neither a full Node.js process nor a browser window.
Why runtimeEnv takes individual references
The runtimeEnv object requires you to list each variable individually:
process.env.DATABASE_URL, it can inline the value statically at build time and exclude it from bundles where it has no value. If you spread the entire process.env object instead, the bundler cannot determine which keys are used, and the whole object - including secrets - can end up in the client bundle.

