Vibestacks LogoVibestacks
Getting Started

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 format

With 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 error

How It Works

1. Schema Definition

Environment variables are defined in src/env.ts using Zod schemas:

src/env.ts
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 url

The 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 suggestion

Server vs Client Variables

t3-env enforces a strict separation between server and client variables:

TypePrefixAccessible FromExample
ServerNoneServer Components, API routesDATABASE_URL, STRIPE_SECRET_KEY
ClientNEXT_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

src/env.ts
export const env = createEnv({
  server: {
    // ... existing vars
    NEW_API_KEY: z.string().min(1),  // Add your new variable
  },
  // ...
});

Step 2: Add to .env.local

.env.local
NEW_API_KEY=your_key_here

Step 3: Use It

import { env } from "@/env"

const apiKey = env.NEW_API_KEY  // Fully typed

Common 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: Required

Fix: 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 dev

TypeScript errors after adding new variable

If TypeScript doesn't recognize a new variable:

  1. Make sure it's added to the schema in src/env.ts
  2. Restart your TypeScript server (VS Code: Cmd+Shift+P → "Restart TS Server")

Benefits Summary

Featureprocess.envt3-env + Zod
Type safetystring | undefinedExact type based on schema
Missing variable detectionRuntime crashBuild-time error
Format validationNoneZod validators
Secret leak preventionNoneClient/server separation
IDE autocompleteLimitedFull autocomplete
Error messagesVagueSpecific and actionable

Next Steps