Type-Safe Environment Variables
Learn how Vibestacks uses t3-env with Zod to validate environment variables at build time, preventing runtime crashes and secret leaks.
Vibestacks uses t3-env with Zod to create type-safe environment variables. This catches missing or invalid variables at build time rather than runtime - preventing production crashes.
Deep Dive
For a detailed explanation of why type-safe env variables matter, read our blog post: Stop Trusting process.env
Why Not Just process.env?
Standard process.env in Node.js has problems:
// ❌ process.env issues
const key = process.env.STRIPE_SECRET_KEY
// TypeScript thinks this is: string | undefined
// If missing, you won't know until runtime
// No validation - could be an invalid formatWith t3-env:
// ✅ t3-env solution
import { env } from "@/env"
const key = env.STRIPE_SECRET_KEY
// TypeScript knows this is: string (guaranteed)
// Missing? Build fails with clear error message
// Invalid format? Build fails with validation errorHow It Works
1. Schema Definition
Environment variables are defined in src/env.ts using Zod schemas:
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
export const env = createEnv({
// Server-side variables (never exposed to browser)
server: {
DATABASE_URL: z.string().url(),
BETTER_AUTH_SECRET: z.string().min(1),
STRIPE_SECRET_KEY: z.string().startsWith("sk_"),
RESEND_API_KEY: z.string().startsWith("re_"),
},
// Client-side variables (safe for browser)
client: {
NEXT_PUBLIC_SITE_URL: z.string().url(),
NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(),
},
// Required for Next.js client variables
experimental__runtimeEnv: {
NEXT_PUBLIC_SITE_URL: process.env.NEXT_PUBLIC_SITE_URL,
NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
},
});2. Build-Time Validation
When you run pnpm build or pnpm dev, t3-env validates all variables:
# Missing variable error
❌ Invalid environment variables:
- STRIPE_SECRET_KEY: Required
# Invalid format error
❌ Invalid environment variables:
- DATABASE_URL: Invalid urlThe build fails immediately with a clear message - no guessing what went wrong.
3. Type-Safe Access
Import env instead of using process.env:
import { env } from "@/env"
// ✅ Fully typed - TypeScript knows the exact type
const dbUrl = env.DATABASE_URL // string (validated as URL)
const secret = env.STRIPE_SECRET_KEY // string (validated with prefix)
// ✅ IDE autocomplete works
env.DA // shows DATABASE_URL suggestionServer vs Client Variables
t3-env enforces a strict separation between server and client variables:
| Type | Prefix | Accessible From | Example |
|---|---|---|---|
| Server | None | Server Components, API routes | DATABASE_URL, STRIPE_SECRET_KEY |
| Client | NEXT_PUBLIC_ | Anywhere (including browser) | NEXT_PUBLIC_SITE_URL |
Leak Prevention
If you try to access a server variable from client code:
// In a Client Component
"use client"
import { env } from "@/env"
// ❌ This throws an error in development
console.log(env.STRIPE_SECRET_KEY)
// "Attempted to access a server-side environment variable on the client"This prevents accidentally leaking secrets to the browser bundle.
Security Feature
Plain process.env doesn't have this protection. If you accidentally reference a secret in client code, it ends up in the browser bundle - visible in DevTools.
Adding New Variables
When you need a new environment variable:
Step 1: Add to Schema
export const env = createEnv({
server: {
// ... existing vars
NEW_API_KEY: z.string().min(1), // Add your new variable
},
// ...
});Step 2: Add to .env.local
NEW_API_KEY=your_key_hereStep 3: Use It
import { env } from "@/env"
const apiKey = env.NEW_API_KEY // Fully typedCommon Zod Validators
Use these validators to ensure variables have the correct format:
// Required string
API_KEY: z.string().min(1),
// URL format
DATABASE_URL: z.string().url(),
// Starts with specific prefix
STRIPE_KEY: z.string().startsWith("sk_"),
// Enum (one of specific values)
NODE_ENV: z.enum(["development", "production", "test"]),
// Number
PORT: z.coerce.number().default(3000),
// Optional with default
OPTIONAL_VAR: z.string().optional().default("default_value"),
// Email format
ADMIN_EMAIL: z.string().email(),
// Boolean (from string)
DEBUG: z.string().transform(s => s === "true"),Troubleshooting
"Invalid environment variables" on build
The error message tells you exactly what's wrong:
❌ Invalid environment variables:
- DATABASE_URL: Invalid url
- STRIPE_SECRET_KEY: RequiredFix: Check your .env.local file for typos or missing values.
Changes not taking effect
Next.js caches environment variables:
# Clear cache and restart
rm -rf .next
pnpm devTypeScript errors after adding new variable
If TypeScript doesn't recognize a new variable:
- Make sure it's added to the schema in
src/env.ts - Restart your TypeScript server (VS Code:
Cmd+Shift+P→ "Restart TS Server")
Benefits Summary
| Feature | process.env | t3-env + Zod |
|---|---|---|
| Type safety | string | undefined | Exact type based on schema |
| Missing variable detection | Runtime crash | Build-time error |
| Format validation | None | Zod validators |
| Secret leak prevention | None | Client/server separation |
| IDE autocomplete | Limited | Full autocomplete |
| Error messages | Vague | Specific and actionable |
Next Steps
- Environment Setup - Configure all required variables
- t3-env Documentation - Official t3-env docs
- Zod Documentation - Learn more about validation schemas
Environment Variables & Configuration
Configure type-safe environment variables with T3 Env. Learn how to securely manage database credentials, authentication secrets, and API keys for local and production environments.
Drizzle ORM & PostgreSQL
Connect and manage your PostgreSQL database using Drizzle ORM. comprehensive guide to local Docker setup, Neon/Supabase integration, and handling schema migrations.