Next.js Environment Variables: .env Hierarchy, Runtime vs Build-Time, Secret Injection, and Vercel
Master Next.js environment variables. Covers .env file hierarchy and precedence, NEXT_PUBLIC_ prefix for client exposure, runtime vs build-time variables, secret injection in Docker and AWS ECS, Vercel environment management, and common mistakes.
Environment variable mistakes are one of the most common causes of Next.js production incidents: secrets accidentally exposed to the browser via NEXT_PUBLIC_, API keys baked into Docker images at build time instead of injected at runtime, and .env files committed to version control. Getting env vars right is foundational security hygiene.
This guide covers the full environment variable system in Next.js โ from file precedence to runtime injection in production.
File Precedence (Lowest to Highest Priority)
.env # Base defaults โ committed to git
.env.local # Local overrides โ NEVER commit (in .gitignore)
.env.development # Development-only โ committed to git
.env.development.local # Local dev overrides โ NEVER commit
.env.production # Production-only โ committed to git (no secrets!)
.env.production.local # Local production overrides โ NEVER commit
.env.test # Test environment
Next.js loads files in this order; later files override earlier ones. The NODE_ENV determines which environment-specific files are loaded.
# .gitignore โ always exclude local files
.env*.local
.env.production # Exclude if it contains any real values
The NEXT_PUBLIC_ Rule
// Variables WITHOUT NEXT_PUBLIC_: server-only (never sent to browser)
DATABASE_URL=postgresql://... // โ
Server only
STRIPE_SECRET_KEY=sk_live_... // โ
Server only
OPENAI_API_KEY=sk-... // โ
Server only
// Variables WITH NEXT_PUBLIC_: embedded in JavaScript bundle at BUILD TIME
NEXT_PUBLIC_APP_URL=https://app.viprasol.com // โ
Safe โ not a secret
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_... // โ
Safe โ publishable key
NEXT_PUBLIC_POSTHOG_KEY=phc_... // โ
Safe โ client analytics key
// โ NEVER do this:
NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_live_... // Exposed to every browser visitor!
NEXT_PUBLIC_DATABASE_URL=postgresql://... // Exposed to every browser visitor!
Critical: NEXT_PUBLIC_ variables are replaced with their literal values at build time by Next.js. If you change them after building, you must rebuild. They cannot be changed at runtime without a new deployment.
๐ Looking for a Dev Team That Actually Delivers?
Most agencies sell you a project manager and assign juniors. Viprasol is different โ senior engineers only, direct Slack access, and a 5.0โ Upwork record across 100+ projects.
- React, Next.js, Node.js, TypeScript โ production-grade stack
- Fixed-price contracts โ no surprise invoices
- Full source code ownership from day one
- 90-day post-launch support included
TypeScript: Validate Env at Startup
// lib/env.ts โ validate all required env vars at module load time
// This crashes the process immediately if config is wrong,
// rather than failing silently later
import { z } from "zod";
const ServerEnvSchema = z.object({
// Database
DATABASE_URL: z.string().url(),
// Auth
NEXTAUTH_URL: z.string().url(),
NEXTAUTH_SECRET: z.string().min(32),
// Stripe
STRIPE_SECRET_KEY: z.string().startsWith("sk_"),
STRIPE_WEBHOOK_SECRET: z.string().startsWith("whsec_"),
// AWS
AWS_REGION: z.string().default("us-east-1"),
AWS_ACCESS_KEY_ID: z.string().optional(),
AWS_SECRET_ACCESS_KEY: z.string().optional(),
S3_BUCKET_NAME: z.string(),
// Email
RESEND_API_KEY: z.string().startsWith("re_"),
// App
NODE_ENV: z.enum(["development", "production", "test"]),
});
const ClientEnvSchema = z.object({
NEXT_PUBLIC_APP_URL: z.string().url(),
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().startsWith("pk_"),
NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(),
NEXT_PUBLIC_POSTHOG_HOST: z.string().url().optional(),
});
// Validate server env (runs only on server โ process.env available)
function validateServerEnv() {
const result = ServerEnvSchema.safeParse(process.env);
if (!result.success) {
console.error("โ Invalid server environment variables:");
console.error(result.error.flatten().fieldErrors);
throw new Error("Invalid environment configuration โ check your .env file");
}
return result.data;
}
// Validate client env (safe to call anywhere)
function validateClientEnv() {
const result = ClientEnvSchema.safeParse({
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,
NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST,
});
if (!result.success) {
throw new Error(`Invalid client env: ${JSON.stringify(result.error.flatten().fieldErrors)}`);
}
return result.data;
}
// Export validated, typed env objects
export const serverEnv = typeof window === "undefined"
? validateServerEnv()
: ({} as ReturnType<typeof validateServerEnv>);
export const clientEnv = validateClientEnv();
// Usage:
// import { serverEnv } from "@/lib/env";
// const stripe = new Stripe(serverEnv.STRIPE_SECRET_KEY);
.env Files Structure
# .env โ safe defaults, committed to git
NODE_ENV=development
NEXT_PUBLIC_APP_URL=http://localhost:3000
# .env.local โ your personal secrets, NEVER commit
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/myapp_dev
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=dev-secret-at-least-32-characters-long
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
AWS_REGION=us-east-1
S3_BUCKET_NAME=myapp-dev-uploads
RESEND_API_KEY=re_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
# .env.production โ production non-secrets only, committed to git
NODE_ENV=production
NEXT_PUBLIC_APP_URL=https://app.viprasol.com
NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com
๐ Senior Engineers. No Junior Handoffs. Ever.
You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.
- MVPs in 4โ8 weeks, full platforms in 3โ5 months
- Lighthouse 90+ performance scores standard
- Works across US, UK, AU timezones
- Free 30-min architecture review, no commitment
Runtime Variables in Docker/ECS
For containerized deployments, never bake secrets into the Docker image. Inject them at runtime:
# Dockerfile โ no ENV instructions for secrets
FROM node:22-alpine AS base
# Build stage: only needs NEXT_PUBLIC_ vars (baked at build time)
FROM base AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
# Build-time NEXT_PUBLIC_ vars are passed as build args
ARG NEXT_PUBLIC_APP_URL
ARG NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
ENV NEXT_PUBLIC_APP_URL=$NEXT_PUBLIC_APP_URL
ENV NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=$NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
RUN npm run build
# Runtime stage: server env vars injected at container start, NOT baked in
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]
# DATABASE_URL, STRIPE_SECRET_KEY, etc. come from ECS task environment
# They are NOT in the image โ they're injected at task start
# terraform/ecs-task.tf โ inject secrets at runtime
resource "aws_ecs_task_definition" "app" {
family = var.app_name
container_definitions = jsonencode([{
name = "app"
image = "${var.ecr_repo}:${var.image_tag}"
environment = [
{ name = "NODE_ENV", value = "production" },
{ name = "AWS_REGION", value = var.aws_region },
# NEXT_PUBLIC_ vars are already baked into the image โ no need to pass again
]
secrets = [
# Fetched from Secrets Manager at container start (not baked into image)
{ name = "DATABASE_URL", valueFrom = "${var.secrets_arn}:DATABASE_URL::" },
{ name = "NEXTAUTH_SECRET", valueFrom = "${var.secrets_arn}:NEXTAUTH_SECRET::" },
{ name = "STRIPE_SECRET_KEY", valueFrom = "${var.secrets_arn}:STRIPE_SECRET_KEY::" },
{ name = "STRIPE_WEBHOOK_SECRET", valueFrom = "${var.secrets_arn}:STRIPE_WEBHOOK_SECRET::" },
{ name = "RESEND_API_KEY", valueFrom = "${var.secrets_arn}:RESEND_API_KEY::" },
]
}])
}
Vercel Environment Management
# Vercel CLI: set environment variables per environment
vercel env add DATABASE_URL production # Production only
vercel env add DATABASE_URL preview # Preview deployments
vercel env add DATABASE_URL development # Local dev via vercel dev
# Pull env vars for local development (safe alternative to .env.local)
vercel env pull .env.local
# List all env vars (values are masked)
vercel env ls
# Remove a variable
vercel env rm OLD_API_KEY production
For Vercel's Edge Runtime (middleware, edge functions), only NEXT_PUBLIC_ vars and variables explicitly marked for Edge are available. Server-only vars are NOT available in Edge:
// middleware.ts โ Edge Runtime
export const runtime = "edge";
// โ
Available in Edge
const appUrl = process.env.NEXT_PUBLIC_APP_URL;
// โ NOT available in Edge (only in Node.js runtime)
// const db = process.env.DATABASE_URL; // undefined at edge
Server Actions and env
// app/actions/send-email.ts โ Server Action
"use server";
// process.env is available in Server Actions (Node.js runtime)
import { serverEnv } from "@/lib/env";
export async function sendWelcomeEmail(userId: string) {
// serverEnv.RESEND_API_KEY is safely available here
const resend = new Resend(serverEnv.RESEND_API_KEY);
// ...
}
Common Mistakes
// โ MISTAKE 1: Reading server env in a Client Component
"use client";
function PaymentForm() {
// process.env.STRIPE_SECRET_KEY is undefined in browser
// And if it were NEXT_PUBLIC_, it would be exposed!
const key = process.env.STRIPE_SECRET_KEY; // undefined
}
// โ
FIX: Call a Server Action or API route that uses the key server-side
// โ MISTAKE 2: Dynamic NEXT_PUBLIC_ variable names
const key = `NEXT_PUBLIC_${name}`; // Won't work โ Next.js replaces statically at build time
process.env[key]; // undefined
// โ
FIX: Reference NEXT_PUBLIC_ vars by their full literal name
// โ MISTAKE 3: Checking NODE_ENV at runtime in Edge
// Edge runtime may not have NODE_ENV set as expected
if (process.env.NODE_ENV === "production") { /* ... */ }
// Use NEXT_PUBLIC_APP_ENV or check the hostname instead at Edge
// โ MISTAKE 4: .env.production with real secrets committed to git
# .env.production
STRIPE_SECRET_KEY=sk_live_abc123 # โ Git history = permanent exposure
// โ
FIX: Use Vercel env vars, AWS Secrets Manager, or inject via CI/CD secrets
Cost and Timeline
Environment variable setup is a one-time configuration task:
| Task | Time |
|---|---|
| .env file hierarchy setup + .gitignore | 30 min |
| Zod env validation schema | 1โ2 hours |
| Docker build-arg vs runtime separation | 1 hour |
| ECS Secrets Manager injection (Terraform) | 2โ3 hours |
| Vercel env management workflow | 30 min |
See Also
- AWS Secrets Manager and Parameter Store
- Next.js App Router Caching Strategies
- AWS ECS Fargate Production Setup
- Next.js Middleware Auth Patterns
- Docker Multi-Stage Builds
Working With Viprasol
Environment variable mistakes are silent until they're catastrophic โ either a secret exposed in a browser bundle, or a missing variable causing a cryptic runtime error in production. Our team sets up typed, validated env schemas with Zod, cleanly separates build-time and runtime variables in Docker/ECS deployments, and manages secrets through AWS Secrets Manager rather than baking them into images.
What we deliver:
- Zod server + client env validation schema (crashes fast on misconfiguration)
.envhierarchy with annotated comments and.gitignoresetup- Dockerfile with build-arg for NEXT_PUBLIC_ and runtime injection for secrets
- Terraform ECS task definition with Secrets Manager
secretsarray - Vercel env management workflow for preview/production/development
Talk to our team about your environment and secrets management โ
Or explore our web development services.
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Need a Modern Web Application?
From landing pages to complex SaaS platforms โ we build it all with Next.js and React.
Free consultation โข No commitment โข Response within 24 hours
Need a custom web application built?
We build React and Next.js web applications with Lighthouse โฅ90 scores, mobile-first design, and full source code ownership. Senior engineers only โ from architecture through deployment.