GetLaunchpad
Back to blog
5 min read

How to manage environment variables in Next.js (the right way)

The difference between NEXT_PUBLIC_ and server-only env vars, how to use .env.local correctly, and the most common mistakes that leak secrets or break production deployments.

Environment variables are how you keep secrets out of your codebase and configuration flexible across environments. Next.js has a built-in env var system that handles most of this automatically — but it has one sharp edge that trips up almost every developer: anything prefixed with NEXT_PUBLIC_ gets bundled into your client-side JavaScript and is visible to every user who opens DevTools. If you put a secret there, it is no longer a secret.

This guide covers the complete pattern for managing environment variables in Next.js: the difference between server and client vars, the .env.local workflow, how to add vars in Vercel, and how to write type-safe access so missing variables fail loudly at startup instead of silently at runtime.

Server-only vs client-visible variables

Next.js splits environment variables into two buckets based on a naming prefix:

The build-time replacement for NEXT_PUBLIC_ vars is important to understand: Next.js literally inlines the value as a string literal during the build. This means if you change a NEXT_PUBLIC_ variable in Vercel after a deployment, the old value is still in the deployed bundle until you trigger a new build.

The .env.local and .env.example pattern

The standard workflow uses two files:

A typical .env.example looks like this:

# Authentication (Clerk)
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_...
CLERK_SECRET_KEY=sk_...

# Database (Supabase)
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
SUPABASE_SERVICE_ROLE_KEY=eyJ...

# Payments (Stripe)
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...

# Email (Resend)
RESEND_API_KEY=re_...

When a new developer clones the repo, they run cp .env.example .env.local and fill in their own values. No secret is ever shared through the repository.

Why you should never put secrets in NEXT_PUBLIC_ variables

This is the mistake that causes real security incidents. Consider what happens if you accidentally write:

# WRONG — this key has full database access
NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY=eyJ...

The service role key bypasses Row Level Security and can read or delete every row in your database. Once you deploy with this prefix, it is embedded in your JavaScript bundle, served to every visitor, and indexable by search engines. Rotating the key is painful — you have to update the value, redeploy, and hope no one captured the old key.

The same applies to STRIPE_SECRET_KEY, RESEND_API_KEY, OPENAI_API_KEY, and any other key that can take destructive action or incur charges. Keep them server-only.

The public Supabase anon key and the Stripe publishable key are designed to be exposed — they have their own security model (RLS and domain restrictions respectively) that assumes they will be seen by clients.

How to add environment variables in the Vercel dashboard

Local .env.local files are only for your development machine. In production, you configure env vars through Vercel:

  1. Open your project in the Vercel dashboard
  2. Go to Settings → Environment Variables
  3. For each variable, set the name, value, and which environments it applies to (Production, Preview, Development)
  4. Click Save — the variables take effect on the next deployment

A few things to know about Vercel env vars:

Type-safe environment variable access

By default, process.env.SOME_VAR returns string | undefined. If you access a missing variable, you get undefined and a runtime error somewhere deep in your code — often a confusing one. The fix is to validate your env vars at startup and fail fast with a clear message.

Create a small validation module that runs when the server starts:

// lib/env.ts
function requireEnv(name: string): string {
  const value = process.env[name];
  if (!value) {
    throw new Error(
      `Missing required environment variable: ${name}. Check your .env.local file or Vercel dashboard.`
    );
  }
  return value;
}

// Server-only vars — import this only in server files
export const env = {
  clerkSecretKey:        requireEnv("CLERK_SECRET_KEY"),
  supabaseServiceKey:    requireEnv("SUPABASE_SERVICE_ROLE_KEY"),
  stripeSecretKey:       requireEnv("STRIPE_SECRET_KEY"),
  stripeWebhookSecret:   requireEnv("STRIPE_WEBHOOK_SECRET"),
  resendApiKey:          requireEnv("RESEND_API_KEY"),
} as const;

Now instead of sprinkling process.env.STRIPE_SECRET_KEY! (the non-null assertion is a red flag) throughout your codebase, you import from lib/env.ts:

import { env } from "@/lib/env";

const stripe = new Stripe(env.stripeSecretKey, { apiVersion: "2024-04-10" });

If STRIPE_SECRET_KEY is missing, your Next.js server will refuse to start and print a clear error. You will catch it in your first deployment rather than in a 2 AM incident when a user tries to check out.

For more robust validation with Zod schemas and TypeScript inference, the t3-env package is worth looking at — but the hand-rolled version above is sufficient for most projects and has zero dependencies.

Common mistakes

Missing variables in production

The most frequent deployment issue. You add a new env var to .env.local, your feature works locally, you deploy — and it crashes in production because you forgot to add the var to Vercel. The type-safe pattern above catches this immediately. Alternatively, add a startup check in your instrumentation.ts (Next.js 15+) that verifies all required vars are present.

Committing .env.local

Check your .gitignore before your first commit. If .env.local is not listed there, add it. If you already committed it by accident, rotate every key in that file immediately — assume the secret is compromised. Then use git filter-repo to scrub the history.

Using NEXT_PUBLIC_ for feature flags you want to change without deploys

Because NEXT_PUBLIC_ vars are inlined at build time, changing them always requires a new build. If you want runtime-configurable feature flags, store them in your database or a service like PostHog feature flags — not in env vars.


The environment variable pattern described here — .env.example for documentation, .env.local for local secrets, Vercel dashboard for production, and a typed validation module — is pre-wired into GetLaunchpad, a Next.js 16 SaaS boilerplate. Every key used by Clerk, Supabase, Stripe, Resend, and PostHog is documented in .env.example and validated at startup.

Share this article:Share on X

Ready to ship faster?

GetLaunchpad gives you everything covered in this guide — pre-configured, tested, and production-ready. Skip the setup and focus on your product.

Get the boilerplate →

More articles