Stop Trusting process.env: Type-Safe Env Variables in Next.js
process.env fails silently and leaks secrets. Here's how t3-env catches missing env vars at build time, not production.

A common issue with junior developers, even experienced ones (and especially the vibe coders!) is trusting that environment variables are always set correctly.
When a project grows, the number of environment variables balloons. APIs, database URLs, secret keys, third-party services... It's easy to miss one or misconfigure it.
Standard process.env in Node.js doesn't help. It's loosely typed, fails silently, and waits until runtime to crash your app.
In this post, I'll show you how we use t3-env in vibestacks to create type-safe environment variables in Next.js - and why it's non-negotiable for any serious SaaS project.
The Problem with process.env
In standard Next.js, TypeScript treats every environment variable as string | undefined.
// TypeScript doesn't know if this exists
const dbUrl = process.env.DATABASE_URL
// So you have to do this everywhere
if (!dbUrl) {
throw new Error("Missing DATABASE_URL")
}
const db = connect(dbUrl)You have to manually check validity in every file. If you forget? TypeScript won't warn you. The build won't fail. The app just explodes at runtime.
When you're shipping fast, you don't have time to write defensive code for every single config variable.
The Solution: Validate at Build Time
t3-env fixes this by forcing you to define a schema for your environment. Think of it like Zod for your secrets.
If a variable is missing or invalid, the build fails immediately. Not "deploys then crashes." It literally won't let you deploy.
Here's how we set it up in src/env.ts:
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
export const env = createEnv({
server: {
// If these are missing, build fails
POSTGRES_URL: z.string().url(),
STRIPE_SECRET_KEY: z.string().min(1),
BETTER_AUTH_SECRET: z.string().min(1),
RESEND_API_KEY: z.string().min(1),
},
client: {
NEXT_PUBLIC_SITE_URL: z.string().url(),
NEXT_PUBLIC_POSTHOG_KEY: z.string().min(1),
},
experimental__runtimeEnv: {
NEXT_PUBLIC_SITE_URL: process.env.NEXT_PUBLIC_SITE_URL,
NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
},
});Now, instead of process.env, you import env:
import { env } from "@/env"
// ✅ Type is 'string', not 'string | undefined'
// ✅ Guaranteed to be a valid URL
const db = connect(env.POSTGRES_URL) No if checks. No ! assertions. Just typed, guaranteed data.
In vibestacks, this is already configured with 20+ environment variables for Stripe, Supabase, Resend, Google Auth, and more. No setup required.
Preventing Leaks (The "Client" Trap)
The most dangerous mistake in Next.js is accidentally leaking a server secret to the client bundle.
If you write console.log(process.env.STRIPE_SECRET) in a Client Component, Next.js usually tries to stop you, but things slip through. Especially if you pass it as a prop.
t3-env creates a hard wall. If you try to access env.STRIPE_SECRET_KEY inside a client-side file, it throws an error immediately during development:
"❌ Attempted to access a server-side environment variable on the client"
This is more common than you'd think, and especially dangerous for vibe coders shipping fast. You use a helper function in a client component, not realizing it references a secret internally. Or you pass a config object as a prop without checking what's inside.
Plain process.env won't stop you - that key ends up in the browser bundle, visible in devtools.
Exposed API keys and database credentials are a gateway for hackers. One of the easiest attack vectors out there. t3-env makes this mistake impossible - the build fails before you can even deploy it.
The Next.js 16 Setup
With Next.js 16, the t3-env setup is cleaner than before. You use experimental__runtimeEnv but only need to list client variables there. The library reads server variables automatically.
Less boilerplate. Same safety.
experimental__runtimeEnv: {
// Only client vars needed here
NEXT_PUBLIC_SITE_URL: process.env.NEXT_PUBLIC_SITE_URL,
NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
},Why This Matters for SaaS Projects
A typical SaaS app needs environment variables for:
- Authentication - OAuth secrets, session keys
- Payments - Stripe secret and webhook keys
- Database - Connection strings
- Email - Resend or SendGrid API keys
- Analytics - PostHog, Sentry tokens
That's 15-25 variables minimum. You will miss one. You will typo one.
Without t3-env, you'd spend an hour debugging why "Sign In" spins forever (spoiler: you missed BETTER_AUTH_URL).
With t3-env, you run pnpm dev and the console tells you exactly what's wrong:
❌ Invalid environment variables:
- STRIPE_SECRET_KEY: Required
- NEXT_PUBLIC_SITE_URL: Invalid urlIt tells you what to fix before you even start coding.
Skip the Setup Entirely
Configuring t3-env properly takes time. You need to:
- Install the package and Zod
- Define schemas for every variable
- Set up the client/server split correctly
- Configure
experimental__runtimeEnvfor Next.js 16 - Update all imports from
process.envtoenv - Create a proper
.env.examplefile
Or you can skip all of it.
vibestacks ships with t3-env pre-configured alongside authentication, Stripe payments, database, and transactional emails. One command, and you're building features instead of config files.
Got questions about t3-env or environment variable setup? Hit me up at support@launcho.dev.
Read more

Tailwind CSS v4: What Changed and Why It's Better
No more tailwind.config.ts. Tailwind v4 moves configuration to CSS, drops JavaScript, and ships 2x faster. Here's everything that changed.

Why We Use cn() and cva() for Component Styling
String concatenation for Tailwind classes is a mess. Here's how cn() and cva() make conditional styling clean, type-safe, and maintainable.

Beyond Pageviews: Why We implemented Analytics into vibestacks PRO
Shipping is only step one. Here's why we pre-configured PostHog to track user intent, not just traffic and why it matters for your growth.