Next.js Adapter
Integrate CtroEnv with Next.js for client/server env variable separation.
Next.js Adapter
The @ctroenv/nextjs package provides Next.js integration with client/server environment
variable splitting and build-time validation.
Installation
npm install @ctroenv/nextjs
Schema Definition
Next.js requires splitting environment variables into server and client schemas:
import { string, number, type ClientServerSchema } from "@ctroenv/core"
import { defineEnv } from "@ctroenv/nextjs"
const schema = {
server: {
DATABASE_URL: string().url(),
JWT_SECRET: string().secret(),
REDIS_URL: string().url().optional(),
},
client: {
NEXT_PUBLIC_API_URL: string().url(),
NEXT_PUBLIC_APP_NAME: string().min(1),
},
} satisfies ClientServerSchema
- Server variables: Only accessible on the server. Accessing from the browser throws an error.
- Client variables: Must be prefixed with
NEXT_PUBLIC_. Accessible everywhere.
defineEnv()
Returns a proxied environment object that enforces server/client boundaries:
import { defineEnv } from "@ctroenv/nextjs"
const env = defineEnv(schema)
const envMasked = defineEnv(schema, { maskWith: "***" })
| Option | Type | Default | Description |
|---|---|---|---|
maskWith | string | "********" | Custom mask string for secret values |
// Server Components (server-side):
env.DATABASE_URL // ✅ string
env.JWT_SECRET // ✅ string
env.NEXT_PUBLIC_API_URL // ✅ string
// Client Components (browser):
env.NEXT_PUBLIC_API_URL // ✅ string
env.NEXT_PUBLIC_APP_NAME // ✅ string
env.DATABASE_URL // ❌ Throws: "Server-only environment variable..."
Server-side
On the server, both server and client schemas are validated against process.env.
Client-side
In the browser, only the client schema is resolved. Server variables return empty
values, and accessing them throws a descriptive error:
Server-only environment variable "DATABASE_URL" is not accessible on the client.
Prefix it with NEXT_PUBLIC_ to expose it to the client bundle.
Accessing Secret Values
Server secrets (.secret()) are masked in the proxy — env.JWT_SECRET returns "********". Access raw values via env.meta.get():
env.meta.get("JWT_SECRET") // actual value
env.meta.has("JWT_SECRET") // true
env.meta.keys() // all env var keys
env.meta.toJSON() // all values unmasked
withCtroEnv()
Validates environment variables at config load time, before your app starts building:
// next.config.ts
import { withCtroEnv } from "@ctroenv/nextjs"
import type { NextConfig } from "next"
const nextConfig: NextConfig = {
// your existing next config
}
export default withCtroEnv(schema, nextConfig)
Validation runs eagerly when the config is loaded, so it works with any bundler — webpack, Turbopack, or whatever comes next. If validation fails, errors are logged and the build exits with code 1.
Composes with any existing webpack function in your config.
Types
import type { InferredNextEnv } from "@ctroenv/nextjs"
// Type alias for InferredClientServerEnv from @ctroenv/core
import type { ClientServerSchema } from "@ctroenv/core"
// Preferred over the deprecated NextSchemaDefinition
NextSchemaDefinitionis deprecated — useClientServerSchemafrom@ctroenv/coreinstead.
Best Practices
- Always validate server-side: Use
withCtroEnv()innext.config.tsto catch issues during build. - Prefix client vars: All client-facing variables must start with
NEXT_PUBLIC_. - Don't expose secrets: Server-only variables are completely inaccessible from the browser — no risk of accidental exposure.
- Type safety: The proxied object provides full TypeScript autocomplete for both server and client variables.
How is this guide?
Last updated on Jun 30, 2026