Define once. Trust everywhere.
TypeScript-first environment variable management toolkit.
A TypeScript-first environment management toolkit. Define your environment schema once — get validation, type inference, CLI tooling, and auto-generated docs across any JavaScript project.
import{defineEnv,string,number,pick}from"@ctroenv/core"constenv=defineEnv({DATABASE_URL: string().url().describe("Primary database connection"),PORT: number().port().default(3000),NODE_ENV: pick(["development","production","test"]),})// Fully inferred types — zero manual annotationsconsole.log(env.PORT)// number — TypeScript knows this| Pillar | What it does |
|---|---|
| Define | Declare variables once with a chainable, type-safe schema — .url(), .email(), .hostname(), .port(), .regex(), .min(), .max(), .int(), .positive(), .secret(), .default(), and more |
| Validate | Fail fast at startup with beautiful terminal errors grouped by category (missing vs invalid), with actionable fix suggestions |
| Document | Auto-generate .env.example, ENVIRONMENT.md, and reference docs from your schema — no more stale documentation |
| Manage | CLI commands for validation, generation, env diffing, CI checks, and scaffolding — plus built-in secret protection for sensitive values |
| Package | Description | Size | Status |
|---|---|---|---|
@ctroenv/core | Schema engine — define, validate, infer types | ~4 KB gzip | Published |
@ctroenv/cli | CLI — validate, generate, check, docs, init | ~17 KB gzip | Published |
@ctroenv/node | Node.js adapter — process.env + .env file loading | ~2 KB gzip | Published |
@ctroenv/vite | Vite adapter — import.meta.env + build plugin | ~2 KB gzip | Published |
@ctroenv/nextjs | Next.js adapter — client/server split, build-time validation | ~3 KB gzip | Published |
@ctroenv/shared | Internal shared utilities — logger, helpers | ~3 KB gzip | Published |
Zero runtime dependencies across all packages. The
@ctroenv/clipackage bundlescommander,dotenv,jiti,chokidar, andpicocolorsfor CLI operations — all other packages remain dependency-free.
For long-running processes, use watchEnv() to re-validate when the source changes:
import{watchEnv,string,number}from"@ctroenv/core"constenv=watchEnv({DATABASE_URL: string().url(),PORT: number().port().default(3000)},{pollInterval: 1000,onChange: (key,old,next)=>console.log(`${key}: ${old} -> ${next}`)},)npm install @ctroenv/core
# or
yarn add @ctroenv/core
# or
pnpm add @ctroenv/core
# or
bun add @ctroenv/coreimport{defineEnv,string,number,pick}from"@ctroenv/core"constenv=defineEnv({// Required with validationDATABASE_URL: string().url().describe("PostgreSQL connection URL"),// Optional with defaultPORT: number().port().default(3000),// Enum-style validationNODE_ENV: pick(["development","production","test"]).default("development"),// Sensitive values — protected from accidental exposureJWT_SECRET: string().secret().min(32),})// TypeScript infers all types automaticallyenv.DATABASE_URL// stringenv.PORT// numberenv.NODE_ENV// "development" | "production" | "test"| Validator | Type | Refinements |
|---|---|---|
string() | string | .url(), .email(), .port(), .hostname(), .min(), .max(), .regex() |
number() | number | .int(), .positive(), .port(), .min(), .max() |
boolean() | boolean | Coerces true/false, "true"/"false", "yes"/"no", "on"/"off", 1/0, "y"/"n", "t"/"f" |
pick([...]) | union literal | Enum validation from a string list |
semver() | string | Strict semver (no ranges, no v prefix) |
ip() / ipv4() / ipv6() | string | IP address validation |
uuid() / guid() | string | UUID (RFC 9562) and permissive GUID |
Every validator supports: .optional(), .default(v), .describe(text), .secret(), .validate(fn)
Sensitive values marked with .secret() are automatically masked in output:
constenv=defineEnv({JWT_SECRET: string().secret()})env.JWT_SECRET// "********"env.meta.get("JWT_SECRET")// actual valueimport{defineSchema,extendSchema}from"@ctroenv/core"constbase=defineSchema({DATABASE_URL: string().url(),PORT: number().port().default(3000),})constschema=extendSchema(base,{JWT_SECRET: string().secret(),})import{createValidator,applyChain,parseOk,singleError,errInvalid,errType}from"@ctroenv/core"functionsemver(){constbase=createValidator<string>((input,ctx)=>{if(typeofinput!=="string")returnsingleError(errType(ctx.key,typeofinput,"semver"))if(!/^\d+\.\d+\.\d+$/.test(input))returnsingleError(errInvalid(ctx.key,input,"not a valid semver"))returnparseOk(input)},{typeLabel: "semver"},)returnapplyChain(base)}# Install globally or use via npx
npm install -g @ctroenv/cli
# Validate your .env against the schema
ctroenv validate
# Watch mode — re-validate on file changes
ctroenv validate --watch
# Generate .env.example from schema
ctroenv generate
# CI-friendly env diffing (exit code 1 on mismatch)
ctroenv check --strict
# Auto-generate ENVIRONMENT.md
ctroenv docs
# Scaffold a new project
ctroenv init
# Generate schema stub from .env file
ctroenv init --from-env .env.local// Node.js — read from process.env + .env filesimport{defineEnv}from"@ctroenv/core"import{loadEnv}from"@ctroenv/node"constenv=defineEnv({DATABASE_URL: string().url()},{source: loadEnv(),})// Vite — read from import.meta.env at build timeimport{defineEnv}from"@ctroenv/core"import{viteSource}from"@ctroenv/vite"constenv=defineEnv({API_URL: string().url()},{source: viteSource(),})// Next.js — client/server split with build-time validationimport{defineEnv}from"@ctroenv/nextjs"constenv=defineEnv({server: {DATABASE_URL: string().url()},client: {NEXT_PUBLIC_API_URL: string().url()},})| Resource | Link |
|---|---|
| Getting Started | ctroenv.vercel.app/docs/getting-started |
| Core API | ctroenv.vercel.app/docs/core |
| CLI Guide | ctroenv.vercel.app/docs/cli |
| Node.js | ctroenv.vercel.app/docs/node |
| Vite | ctroenv.vercel.app/docs/vite |
| Next.js | ctroenv.vercel.app/docs/nextjs |
| Migration Guides | ctroenv.vercel.app/docs/migration |
Check the examples/ directory for complete project setups:
- Basic Node.js
- Express + CtroEnv
- Vite + CtroEnv
- Next.js + CtroEnv
- CLI-driven project
- Monorepo setup
- GitHub Actions
| Feature | CtroEnv | Zod + manual | envalid | t3-env |
|---|---|---|---|---|
| Zero-dep core | ✅ | ❌ (Zod ~50 KB) | ❌ (6 deps) | ❌ (Zod + t3) |
| Full type inference | ✅ | ✅ | Partial | ✅ |
| Built-in CLI | ✅ | ❌ | ❌ | ❌ |
| Beautiful errors | ✅ | ❌ | Basic | Basic |
| Framework adapters | Node, Vite, Next.js | Manual | Node only | Next.js only |
| Auto-docs | ✅ | ❌ | ❌ | ❌ |
We welcome contributions! See CONTRIBUTING.md for guidelines.
This project uses:
- npm workspaces for the monorepo
- Biome for linting and formatting
- Vitest for testing (v8 coverage, 90%+ thresholds)
- tsup for building
git clone https://github.com/ctrotech-tutor/ctroenv.git
cd ctroenv
npm install
npm run build
npm testSee SECURITY.md for our security policy and responsible disclosure process.
MIT © Ctrotech
Documentation • GitHub • npm
