CtroEnv
ctroenvType-Safe Environment Variables
Getting StartedQuick StartCore Concepts
defineEnv()string()number()boolean()semver()pick()ip(), ipv4(), ipv6()uuid(), guid()Chainable MethodsRefinementsError HandlingSchema CompositionSecurityCustom ValidatorswatchEnv()
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, 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: "***" })
OptionTypeDefaultDescription
maskWithstring"********"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

NextSchemaDefinition is deprecated — use ClientServerSchema from @ctroenv/core instead.

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 30, 2026

PreviousVite AdapterNextMigration from t3-env

On this page

InstallationSchema DefinitiondefineEnv()Server-sideClient-sideAccessing Secret ValueswithCtroEnv()TypesBest Practices