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

Migration from t3-env

Migrate from t3-env to CtroEnv with this side-by-side comparison guide.

  1. Docs
  2. Migration

Migration from t3-env

CtroEnv is heavily inspired by t3-env and shares the same chainable validator pattern. Migration is straightforward.

Key Differences

Featuret3-envCtroEnv
Runtime depszod (required)Zero dependencies
Framework adaptersBuilt-inSeparate packages (@ctroenv/node, @ctroenv/vite, @ctroenv/nextjs)
CLINoneFull CLI (validate, generate, check, docs, init)
Error messagesBasicRich, grouped, with suggestions
Secret maskingNot supported.secret() method
Type inferenceVia zod inferenceNative inference
Bundle size~50KB+ (zod included)under 5KB gzipped
ValidationThrows on first errorCollects all errors

Step-by-Step Migration

1. Install CtroEnv

npm uninstall @t3-oss/env-core
npm install @ctroenv/core

2. Replace imports

// Before (t3-env)
import { createEnv } from "@t3-oss/env-core"
import { z } from "zod"

// After (CtroEnv)
import { defineEnv, string, number, pick } from "@ctroenv/core"

3. Replace schema definitions

// Before (t3-env)
export const env = createEnv({
  server: {
    DATABASE_URL: z.string().url(),
    PORT: z.coerce.number().int().positive(),
    NODE_ENV: z.enum(["dev", "prod"]),
  },
  client: {
    NEXT_PUBLIC_API_URL: z.string().url(),
  },
  clientPrefix: "NEXT_PUBLIC_",
  runtimeEnv: process.env,
})

// After (CtroEnv — standalone)
const env = defineEnv({
  DATABASE_URL: string().url(),
  PORT: number().int().positive(),
  NODE_ENV: pick(["dev", "prod"]),
})

// After (CtroEnv — Next.js)
import { string, number, pick } from "@ctroenv/core"
import { defineEnv } from "@ctroenv/nextjs"

const env = defineEnv({
  server: {
    DATABASE_URL: string().url(),
    PORT: number().int().positive(),
    NODE_ENV: pick(["dev", "prod"]),
  },
  client: {
    NEXT_PUBLIC_API_URL: string().url(),
  },
})

4. Update adapters

// Before
import { createEnv } from "@t3-oss/env-core"
import { z } from "zod"

// After
import { defineEnv } from "@ctroenv/core"
import { loadEnv } from "@ctroenv/node"

const env = defineEnv(schema, {
  source: loadEnv(),
})

What's Different

Error Collection

t3-env throws on the first error. CtroEnv collects ALL errors:

// t3-env: throws on first missing variable (you fix, re-run, repeat)
// CtroEnv: throws with ALL errors at once (fix everything in one pass)

No .coerce()

t3-env uses z.coerce.number() for string coercion. CtroEnv's number() handles coercion automatically:

// t3-env
z.coerce.number()  // Requires explicit coercion

// CtroEnv
number()  // Auto-coerces numeric strings

How is this guide?

Edit on GitHub

Last updated on Jun 24, 2026

PreviousNext.js AdapterNextMigration from envalid

On this page

Key DifferencesStep-by-Step Migration1. Install CtroEnv2. Replace imports3. Replace schema definitions4. Update adaptersWhat's DifferentError CollectionNo `.coerce()`