CtroEnv
ctroenvType-Safe Environment Variables
Getting StartedQuick StartCore Concepts
defineEnv()string()number()boolean()pick()Chainable MethodsRefinementsError HandlingSchema Composition
CLI Overviewctroenv validatectroenv generatectroenv checkctroenv docsctroenv initCLI Configuration
Node AdapterVite AdapterNext.js Adapter
Migration from t3-envMigration from envalidMigration from dotenv

Next.js Adapter

Integrate CtroEnv with Next.js for client/server env variable separation.

  1. Docs
  2. Adapters

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 } from "@ctroenv/core"
import { defineEnv, type NextSchemaDefinition } 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 NextSchemaDefinition
  • 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)

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

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.

Type Inference

The InferredNextEnv type automatically infers the shape from your schema:

type InferredNextEnv<T extends NextSchemaDefinition> = {
  [K in keyof T["server"]]: T["server"][K] extends Validator<infer V> ? V : never
} & {
  [K in keyof T["client"]]: T["client"][K] extends Validator<infer V> ? V : never
}

// Full autocomplete for env.DATABASE_URL, env.NEXT_PUBLIC_API_URL, etc.

Best Practices

  1. Always validate server-side: Use withCtroEnv() in next.config.ts to catch issues during build.
  2. Prefix client vars: All client-facing variables must start with NEXT_PUBLIC_.
  3. Don't expose secrets: Server-only variables are completely inaccessible from the browser — no risk of accidental exposure.
  4. Type safety: The proxied object provides full TypeScript autocomplete for both server and client variables.

How is this guide?

Edit on GitHub

Last updated on Jun 24, 2026

PreviousVite AdapterNextMigration from t3-env

On this page

InstallationSchema DefinitiondefineEnv()Server-sideClient-sidewithCtroEnv()Type InferenceBest Practices