Skip to main content
JSON & Schemas · 100% Client-Side Engine

.env to Zod & T3 Env Schema Generator

Convert .env files into Zod v4, Zod v3, or @t3-oss/env-nextjs schemas in your browser. Infers types without exposing secret values and generates .env.example.

.env to Zod & T3 Env100% Client-Side Engine
Output Configuration Options
Choose Zod v4, Zod v3, T3 Env, or safe .env.example
Destructure client-only or all variables in T3 Env mode
Map NODE_ENV to z.enum(['development', 'production', 'test'])
Exported Zod schema variable name
Input (dotenv)14 lines
Output (typescript)25 lines

Example Output (.env to Zod & T3 Env)

SAMPLE INPUT (DOTENV)
# Server & Database Configuration (Dummy Sample Values)
NODE_ENV=development
PORT=3000
DATABASE_URL=postgresql://db_user:dummy_password@localhost:5432/app_dev
REDIS_URL=redis://localhost:6379
STRIPE_SECRET_KEY=sk_live_dummy_key_val
STRIPE_WEBHOOK_SECRET=whsec_dummy_secret_val
ENABLE_RATE_LIMIT=true
ADMIN_EMAIL=ops@example.com
OPTIONAL_SENTRY_DSN=

# Next.js Public Client Variables
NEXT_PUBLIC_APP_URL=https://app.example.com
NEXT_PUBLIC_ENABLE_ANALYTICS=false
GENERATED OUTPUT (TYPESCRIPT)
// Target: Zod v4 (Stable) — Values scrubbed, only types inferred
import { z } from "zod";

export const envSchema = z.object({
  /** Server & Database Configuration (Dummy Sample Values) */
  NODE_ENV: z.enum(["development", "production", "test"]),
  PORT: z.coerce.number().int().positive(),
  DATABASE_URL: z.url(),
  REDIS_URL: z.url(),
  /** ⚠️ Sensitive — never log or expose to client bundles */
  STRIPE_SECRET_KEY: z.string().min(1),
  /** ⚠️ Sensitive — never log or expose to client bundles */
  STRIPE_WEBHOOK_SECRET: z.string().min(1),
  ENABLE_RATE_LIMIT: z.stringbool(),
  ADMIN_EMAIL: z.email(),
  OPTIONAL_SENTRY_DSN: z.string().optional(),
  /** Next.js Public Client Variables */
  NEXT_PUBLIC_APP_URL: z.url(),
  NEXT_PUBLIC_ENABLE_ANALYTICS: z.stringbool(),
});

export type Env = z.infer<typeof envSchema>;

export const env = envSchema.parse(process.env);

Understanding the .env to Zod & T3 Env Transformation

Environment variables in Node.js and Next.js are always loaded from .env files as raw strings (or undefined when missing). Relying on unvalidated process.env access scatters string-to-number and string-to-boolean conversions across your codebase and risks runtime failures when a required secret or connection string is missing in production. Generating a strict Zod v4, Zod v3, or @t3-oss/env-nextjs schema validates your configuration at application startup while keeping raw secret values completely out of your source code.

Key Architectural Benefits

  • •Zero secret exposure: The engine inspects value patterns locally in your browser to infer types (URLs, ports, booleans, emails) and never prints raw secret values in the generated code.
  • •Eliminates the repetitive @t3-oss/env-nextjs boilerplate of declaring every variable across server, client, and runtimeEnv blocks.
  • •Prevents the classic JavaScript Boolean('false') === true bug by using z.stringbool() in Zod v4 or strict enum transforms in Zod v3.
  • •Flags accidental NEXT_PUBLIC_ secret leaks (such as NEXT_PUBLIC_STRIPE_SECRET_KEY) before they are bundled into browser JavaScript.

Common Conversion Pitfalls & Fixes

⚠️ Pitfall: Calling envSchema.parse(process.env) inside Next.js Client Components
Solution: Next.js bundlers (Webpack and Turbopack) do not pass the full process.env object to the browser; they only inline literal property accesses like process.env.NEXT_PUBLIC_APP_URL at build time. In the browser, process.env evaluates to an empty object {}, causing envSchema.parse(process.env) to throw a ZodError for every variable. Use @t3-oss/env-nextjs with experimental__runtimeEnv (or explicit runtimeEnv destructuring) so Next.js can inline NEXT_PUBLIC_ variables safely.
⚠️ Pitfall: Using z.coerce.boolean() on environment variables (turns "false" into true)
Solution: Under the hood, z.coerce.boolean() calls JavaScript's Boolean(value) constructor. Because any non-empty string in JavaScript is truthy, Boolean("false") and Boolean("0") both evaluate to true! Always use z.stringbool() in Zod v4 or z.enum(["true", "false"]).transform((v) => v === "true") in Zod v3.
⚠️ Pitfall: Typing every variable three times manually in @t3-oss/env-nextjs
Solution: When configuring createEnv() manually, developers have to maintain server keys, client NEXT_PUBLIC_ keys, and the runtimeEnv mapping in sync. Switching the Output Target to '@t3-oss/env-nextjs (createEnv)' partitions server and client keys automatically and generates either experimental__runtimeEnv (for Next.js >= 13.4.4 client keys) or full runtimeEnv.

Verified Edge Cases & Complex Scenarios

Real payloads often contain edge cases that break naive generators. Here is how our engine handles them:

1. Non-HTTP Connection Strings (postgresql://, redis://, mongodb+srv://)

Database and cache URLs use custom URI schemes rather than http:// or https://, which naive regex matchers misclassify as plain strings.

INPUT (DOTENV)
DATABASE_URL=postgresql://user:pass@localhost:5432/prod
REDIS_URL=redis://127.0.0.1:6379
EMPTY_OPTIONAL_KEY=
OUTPUT (TYPESCRIPT)
export const envSchema = z.object({
  DATABASE_URL: z.url(),
  REDIS_URL: z.url(),
  EMPTY_OPTIONAL_KEY: z.string().optional(),
});
Why: Our parser recognizes any valid RFC-3986 URI scheme (postgresql://, redis://, amqp://, wss://) as a URL validator and maps empty keys to z.string().optional().
2. Quoted Hash Characters (#), export Prefixes, and Multiline Keys

Shell-compatible .env files often prefix variables with `export`, include `#` characters inside quoted passwords, or span PEM keys across multiple lines.

INPUT (DOTENV)
export API_PORT=8080 # inline comment
REDIS_PASSWORD="p@ss#word#with#hash"
JWT_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC
-----END PRIVATE KEY-----"
OUTPUT (TYPESCRIPT)
export const envSchema = z.object({
  API_PORT: z.coerce.number().int().positive(),
  /** ⚠️ Sensitive — never log or expose to client bundles */
  REDIS_PASSWORD: z.string().min(1),
  /** ⚠️ Sensitive — never log or expose to client bundles */
  JWT_PRIVATE_KEY: z.string().min(1),
});
Why: The lexer strips `export `, ignores `#` comments outside quotes while preserving `#` inside quotes, collapses multiline quoted strings into a single variable, and never leaks the raw secret into the output.
3. Detecting Accidental NEXT_PUBLIC_ Secret Leaks

Prefixing a sensitive API key or webhook secret with NEXT_PUBLIC_ causes Next.js to inline the secret directly into public client JavaScript bundles.

INPUT (DOTENV)
NEXT_PUBLIC_APP_URL=https://castov.com
NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_live_0000000000
OUTPUT (TYPESCRIPT)
// ⚠️ PUBLIC SECRET WARNING (NEXT_PUBLIC_STRIPE_SECRET_KEY): Variables prefixed with NEXT_PUBLIC_ are inlined into client-side JavaScript bundles!
export const envSchema = z.object({
  NEXT_PUBLIC_APP_URL: z.url(),
  /** ⚠️ SECURITY WARNING: NEXT_PUBLIC_ exposes 'NEXT_PUBLIC_STRIPE_SECRET_KEY' to browser bundles! */
  NEXT_PUBLIC_STRIPE_SECRET_KEY: z.string().min(1),
});
Why: Whenever a NEXT_PUBLIC_ variable name contains SECRET, KEY, TOKEN, or PASSWORD, both the code output and UI banner trigger an immediate security warning.

Frequently Asked Questions

Are my .env secret values ever sent to a server or included in the output?
No. Conversion runs 100% client-side in your browser with zero network requests. Furthermore, our generator only inspects the format of each value (to determine whether it is a URL, integer, boolean, or email) and discards the value itself—your actual secrets never appear in the generated Zod schema.
Why does z.coerce.boolean() fail on 'false' in .env files?
In JavaScript, process.env values are strings, and z.coerce.boolean() passes the raw string to Boolean(val). Since Boolean('false') is true for any non-empty string, setting FEATURE_FLAG=false in your .env file evaluates to true at runtime! Our Zod v4 mode outputs z.stringbool() and our Zod v3 mode outputs z.enum(['true', 'false']).transform((v) => v === 'true') to handle boolean strings safely.
What is the difference between runtimeEnv and experimental__runtimeEnv in @t3-oss/env-nextjs?
In Next.js < 13.4.4, you had to manually list every server and client variable inside runtimeEnv. Starting in Next.js 13.4.4, you only need to destructure client-side NEXT_PUBLIC_ variables inside experimental__runtimeEnv while server variables are read automatically from process.env.
How can I generate a clean .env.example file to commit to Git?
Click the 'Generate Safe .env.example' button in the toolbar or select 'Safe .env.example (Scrubbed)' in Output Target. It preserves your section comments and variable names while replacing real credentials with safe dummy placeholders.
Browse All