Environment variables let you configure your application without hard-coding values. Store API keys, database URLs, feature flags, and other configuration safely. Next.js supports .env files with automatic loading, NEXT_PUBLIC_ prefix for browser exposure, and environment-specific files. Master environment variables and build secure, configurable applications!
Environment Variable Files
File Types and Priority
Priority Order (highest to lowest):
- .env.local - Local overrides (never commit)
- .env.[environment] - Environment-specific (.env.development, .env.production)
- .env - Base variables (all environments)
Special Files:
- .env.example - Template showing required variables (commit this!)
- .env.test - Test environment variables (when NODE_ENV=test)
Basic .env File
# Database
DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
# API Keys
API_KEY="your-api-key-here"
STRIPE_SECRET_KEY="sk_test_..."
# App Configuration
APP_NAME="My Next.js App"
APP_URL="http://localhost:3000"
# Feature Flags
ENABLE_ANALYTICS=true
# ✅ All environments use these as defaults
# ✅ Can be overridden by .env.local or .env.production
# ✅ Commit this file to version controlLocal Overrides (.env.local)
# Local development overrides
DATABASE_URL="postgresql://localhost:5432/mydb_dev"
API_KEY="local-development-key"
# Local secrets (NEVER commit this file!)
STRIPE_SECRET_KEY="sk_test_local_..."
JWT_SECRET="local-jwt-secret-123"
# ✅ Highest priority
# ✅ Overrides .env values
# ✅ NEVER commit to version control
# ✅ Add to .gitignoreEnvironment-Specific Files
# Development environment
DATABASE_URL="postgresql://localhost:5432/mydb_dev"
API_URL="http://localhost:3000/api"
ENABLE_DEBUG=true
# ✅ Only loaded when NODE_ENV=development
# ✅ Overrides .env
# ✅ Overridden by .env.local# Production environment
DATABASE_URL="postgresql://prod-server:5432/mydb_prod"
API_URL="https://api.myapp.com"
ENABLE_DEBUG=false
# ✅ Only loaded when NODE_ENV=production
# ✅ Overrides .env
# ✅ Overridden by .env.localTemplate File (.env.example)
# Database Configuration
DATABASE_URL="postgresql://user:password@host:5432/database"
# API Keys
API_KEY="your-api-key"
STRIPE_SECRET_KEY="sk_..."
# App Configuration
APP_NAME="My App"
APP_URL="http://localhost:3000"
# Feature Flags
ENABLE_ANALYTICS=true
# ✅ Shows required variables
# ✅ No real values
# ✅ Commit to version control
# ✅ Team members copy to .env.localAccessing Environment Variables
Server-Side Access
export async function GET() {
// Access environment variables
const dbUrl = process.env.DATABASE_URL;
const apiKey = process.env.API_KEY;
const jwtSecret = process.env.JWT_SECRET;
// All variables available on server
return Response.json({
hasDbUrl: !!dbUrl,
hasApiKey: !!apiKey,
hasJwtSecret: !!jwtSecret,
// ⚠️ Never expose actual secrets!
});
}
// ✅ Available in Route Handlers
// ✅ Available in Server Components
// ✅ Available in Server Actions
// ✅ Available in MiddlewareServer Component Access
export default async function DashboardPage() {
// Access environment variables
const apiUrl = process.env.API_URL;
const enableDebug = process.env.ENABLE_DEBUG === 'true';
// Fetch data using env vars
const data = await fetch(`${apiUrl}/data`, {
headers: {
'Authorization': `Bearer ${process.env.API_KEY}`,
},
}).then(res => res.json());
return (
<div>
<h1>Dashboard</h1>
{enableDebug && <pre>{JSON.stringify(data, null, 2)}</pre>}
</div>
);
}
// ✅ All env vars available in Server Components
// ✅ Secure - not exposed to browserServer Action Access
'use server';
export async function createData(formData: FormData) {
const title = formData.get('title');
// Access environment variables
const dbUrl = process.env.DATABASE_URL;
const apiKey = process.env.API_KEY;
// Use in database operations
const db = await connectToDatabase(dbUrl);
const result = await db.data.create({ title });
return { success: true, id: result.id };
}
// ✅ Available in Server Actions
// ✅ Secure server-side executionNEXT_PUBLIC_ Variables
Defining Public Variables
# Server-only (not exposed to browser)
API_KEY="secret-key-123"
DATABASE_URL="postgresql://..."
# Public (exposed to browser)
NEXT_PUBLIC_API_URL="https://api.myapp.com"
NEXT_PUBLIC_ANALYTICS_ID="UA-123456"
NEXT_PUBLIC_ENABLE_FEATURE="true"
# ✅ NEXT_PUBLIC_ prefix makes variables public
# ✅ Available in Client Components
# ✅ Bundled into client JavaScript
# ⚠️ Never use for secrets!Accessing in Client Components
'use client';
export function Analytics() {
// ✅ Available in Client Components
const analyticsId = process.env.NEXT_PUBLIC_ANALYTICS_ID;
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
// ❌ NOT available (undefined)
const apiKey = process.env.API_KEY;
const dbUrl = process.env.DATABASE_URL;
React.useEffect(() => {
if (analyticsId) {
// Initialize analytics
initAnalytics(analyticsId);
}
}, [analyticsId]);
return <div>Analytics initialized</div>;
}
// ✅ Only NEXT_PUBLIC_ vars available
// ✅ Safe for browser
// ⚠️ Everyone can see these valuesRuntime Access in Browser
'use client';
export function ApiClient() {
async function fetchData() {
// ✅ Available at runtime
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
const response = await fetch(`${apiUrl}/data`);
const data = await response.json();
return data;
}
return (
<button onClick={fetchData}>
Fetch Data
</button>
);
}
// ✅ NEXT_PUBLIC_ vars embedded in bundle
// ✅ Available in browser DevTools
// ⚠️ Visible to all users⚠️ NEVER Use NEXT_PUBLIC_ for Secrets
NEXT_PUBLIC_ variables are embedded in the client JavaScript bundle and visible to anyone. Never use them for:
- API keys for server-to-server communication
- Database credentials
- JWT secrets
- Payment gateway secrets
- Any sensitive configuration
Use NEXT_PUBLIC_ only for non-sensitive configuration like API URLs, feature flags, or analytics IDs.
Type-Safe Environment Variables
Environment Validation with Zod
import { z } from 'zod';
// Define schema
const envSchema = z.object({
// Server-only
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(1),
JWT_SECRET: z.string().min(32),
STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
// Public
NEXT_PUBLIC_API_URL: z.string().url(),
NEXT_PUBLIC_ANALYTICS_ID: z.string().optional(),
NEXT_PUBLIC_ENABLE_FEATURE: z
.string()
.transform(val => val === 'true')
.default('false'),
// App config
NODE_ENV: z.enum(['development', 'production', 'test']),
});
// Validate and export
export const env = envSchema.parse({
DATABASE_URL: process.env.DATABASE_URL,
API_KEY: process.env.API_KEY,
JWT_SECRET: process.env.JWT_SECRET,
STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
NEXT_PUBLIC_ANALYTICS_ID: process.env.NEXT_PUBLIC_ANALYTICS_ID,
NEXT_PUBLIC_ENABLE_FEATURE: process.env.NEXT_PUBLIC_ENABLE_FEATURE,
NODE_ENV: process.env.NODE_ENV,
});
// Type-safe exports
export type Env = z.infer<typeof envSchema>;
// ✅ Validates on startup
// ✅ Type-safe access
// ✅ Catches missing variables early
// ✅ Transforms values (string → boolean)Using Validated Variables
import { env } from './env';
export async function connectToDatabase() {
// ✅ Type-safe and validated
const dbUrl = env.DATABASE_URL;
const connection = await createConnection({
url: dbUrl,
});
return connection;
}
// ✅ TypeScript knows env.DATABASE_URL is a string
// ✅ Validation failed if variable missing
// ✅ No runtime undefined errorsAlternative: t3-env
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
export const env = createEnv({
// Server-side environment variables
server: {
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(1),
JWT_SECRET: z.string().min(32),
},
// Client-side environment variables (NEXT_PUBLIC_)
client: {
NEXT_PUBLIC_API_URL: z.string().url(),
NEXT_PUBLIC_ANALYTICS_ID: z.string().optional(),
},
// Values from process.env
runtimeEnv: {
DATABASE_URL: process.env.DATABASE_URL,
API_KEY: process.env.API_KEY,
JWT_SECRET: process.env.JWT_SECRET,
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
NEXT_PUBLIC_ANALYTICS_ID: process.env.NEXT_PUBLIC_ANALYTICS_ID,
},
});
// ✅ Validates server/client separately
// ✅ Type-safe
// ✅ Enforces NEXT_PUBLIC_ prefix
// ✅ Popular in T3 StackEnvironment Files Structure
Organization of .env files and configuration
Select a file or folder to see details
Common Configuration Patterns
Database Configuration
# Development
DATABASE_URL="postgresql://localhost:5432/myapp_dev"
DATABASE_POOL_SIZE=10
# Production (in .env.production or hosting platform)
# DATABASE_URL="postgresql://prod-server/myapp"
# DATABASE_POOL_SIZE=50import { PrismaClient } from '@prisma/client';
const globalForPrisma = global as unknown as {
prisma: PrismaClient | undefined;
};
export const db =
globalForPrisma.prisma ??
new PrismaClient({
datasourceUrl: process.env.DATABASE_URL,
log: process.env.NODE_ENV === 'development'
? ['query', 'error', 'warn']
: ['error'],
});
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = db;
}
// ✅ Uses DATABASE_URL from env
// ✅ Different logging per environment
// ✅ Singleton patternAPI Configuration
# API Endpoints
API_URL="http://localhost:3000/api"
EXTERNAL_API_URL="https://api.external.com"
EXTERNAL_API_KEY="your-api-key"
# Timeouts (milliseconds)
API_TIMEOUT=5000
# Retry Configuration
API_MAX_RETRIES=3const API_URL = process.env.API_URL;
const API_TIMEOUT = parseInt(process.env.API_TIMEOUT || '5000', 10);
const MAX_RETRIES = parseInt(process.env.API_MAX_RETRIES || '3', 10);
export async function fetchWithRetry(
endpoint: string,
options?: RequestInit,
retries = MAX_RETRIES
): Promise<Response> {
const url = `${API_URL}${endpoint}`;
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), API_TIMEOUT);
const response = await fetch(url, {
...options,
signal: controller.signal,
});
clearTimeout(timeout);
return response;
} catch (error) {
if (retries > 0) {
await new Promise(resolve => setTimeout(resolve, 1000));
return fetchWithRetry(endpoint, options, retries - 1);
}
throw error;
}
}
// ✅ Configured via env vars
// ✅ Timeout and retry logic
// ✅ Easy to change per environmentFeature Flags
# Feature Flags
NEXT_PUBLIC_ENABLE_ANALYTICS=true
NEXT_PUBLIC_ENABLE_NEW_UI=false
ENABLE_EXPERIMENTAL_FEATURES=false
# A/B Testing
NEXT_PUBLIC_AB_TEST_VARIANT=controlexport const features = {
analytics: process.env.NEXT_PUBLIC_ENABLE_ANALYTICS === 'true',
newUI: process.env.NEXT_PUBLIC_ENABLE_NEW_UI === 'true',
experimental: process.env.ENABLE_EXPERIMENTAL_FEATURES === 'true',
abTestVariant: process.env.NEXT_PUBLIC_AB_TEST_VARIANT || 'control',
} as const;
export function isFeatureEnabled(feature: keyof typeof features): boolean {
return features[feature] === true;
}
// ✅ Centralized feature flags
// ✅ Type-safe access
// ✅ Easy to toggle'use client';
import { features } from '@/lib/features';
export function ConditionalFeature() {
if (!features.newUI) {
return <OldUI />;
}
return <NewUI />;
}
// ✅ Toggle features with env vars
// ✅ No code changes needed
// ✅ Different per environmentAuthentication Configuration
# JWT Configuration
JWT_SECRET="your-super-secret-jwt-key-min-32-chars"
JWT_EXPIRES_IN="7d"
# OAuth
GOOGLE_CLIENT_ID="your-google-client-id"
GOOGLE_CLIENT_SECRET="your-google-client-secret"
GITHUB_CLIENT_ID="your-github-client-id"
GITHUB_CLIENT_SECRET="your-github-client-secret"
# Callback URLs
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="your-nextauth-secret"import { SignJWT } from 'jose';
const JWT_SECRET = new TextEncoder().encode(
process.env.JWT_SECRET
);
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d';
export async function createToken(userId: string) {
const token = await new SignJWT({ userId })
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime(JWT_EXPIRES_IN)
.sign(JWT_SECRET);
return token;
}
// ✅ Configured via env vars
// ✅ Different secrets per environment
// ✅ Configurable expirationEnvironment Variables Best Practices
1. Use .env.example as Template
# Copy this file to .env.local and fill in your values
# Required
DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
JWT_SECRET="your-secret-here-min-32-chars"
# Optional
NEXT_PUBLIC_ANALYTICS_ID="UA-XXXXXXX"
ENABLE_DEBUG=false
# ✅ Commit .env.example
# ✅ Shows required variables
# ✅ No real values
# ✅ Team members copy to .env.local2. Add .env.local to .gitignore
# Environment variables
.env.local
.env*.local
# Keep .env and .env.example
# !.env
# !.env.example
# ✅ Never commit secrets
# ✅ Each developer has their own .env.local3. Validate on Startup
import { z } from 'zod';
const envSchema = z.object({
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
// ... other variables
});
// Validate immediately
const result = envSchema.safeParse(process.env);
if (!result.success) {
console.error('❌ Invalid environment variables:');
console.error(result.error.flatten().fieldErrors);
throw new Error('Invalid environment variables');
}
export const env = result.data;
// ✅ Fail fast if config invalid
// ✅ Clear error messages
// ✅ Prevents runtime errors4. Never Hard-Code Secrets
// ❌ BAD: Hard-coded secrets
const API_KEY = 'sk_live_abc123'; // NEVER!
const DB_URL = 'postgresql://user:password@...'; // NEVER!
// ✅ GOOD: Environment variables
const API_KEY = process.env.API_KEY;
const DB_URL = process.env.DATABASE_URL;
// Always use environment variables for secrets5. Use Different Values Per Environment
# .env.development
DATABASE_URL="postgresql://localhost:5432/myapp_dev"
API_URL="http://localhost:3000/api"
ENABLE_DEBUG=true
# .env.production
DATABASE_URL="postgresql://prod-server/myapp_prod"
API_URL="https://api.myapp.com"
ENABLE_DEBUG=false
# ✅ Different configs per environment
# ✅ Appropriate settings for each6. Document Required Variables
## Environment Variables
Copy `.env.example` to `.env.local` and configure:
### Required
- `DATABASE_URL`: PostgreSQL connection string
- `JWT_SECRET`: Secret for JWT signing (min 32 chars)
- `STRIPE_SECRET_KEY`: Stripe API secret key
### Optional
- `NEXT_PUBLIC_ANALYTICS_ID`: Google Analytics ID
- `ENABLE_DEBUG`: Enable debug logging (default: false)
### Getting API Keys
- Stripe: https://dashboard.stripe.com/apikeys
- Google Analytics: https://analytics.google.com
# ✅ Clear documentation
# ✅ Helps team members
# ✅ Links to get credentialsDeployment Configuration
Vercel Deployment
# Set environment variables in Vercel dashboard:
# Settings → Environment Variables
# Or use Vercel CLI:
vercel env add DATABASE_URL production
vercel env add JWT_SECRET production
vercel env add NEXT_PUBLIC_API_URL production
# Pull environment variables locally:
vercel env pull .env.local
# ✅ Secure storage in Vercel
# ✅ Environment-specific values
# ✅ Encrypted at restDocker Deployment
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
# Build with environment variables from build args
ARG DATABASE_URL
ARG NEXT_PUBLIC_API_URL
ENV DATABASE_URL=${DATABASE_URL}
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]# Build with environment variables
docker build \
--build-arg DATABASE_URL="postgresql://..." \
--build-arg NEXT_PUBLIC_API_URL="https://api.myapp.com" \
-t myapp .
# Or use docker-compose.yml with .env file
docker-compose up
# ✅ Environment variables at build time
# ✅ Secure secret managementGitHub Actions Secrets
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build and deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
JWT_SECRET: ${{ secrets.JWT_SECRET }}
NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }}
run: |
npm ci
npm run build
npm run deploy
# ✅ Secrets in GitHub repository settings
# ✅ Not exposed in logs
# ✅ Encrypted storageKey Takeaways
- .env files - automatic loading, multiple environments
- Priority order - .env.local → .env.[environment] → .env
- Server-only by default - process.env in server contexts
- NEXT_PUBLIC_ prefix - expose to browser (never for secrets!)
- Validate with Zod - type-safe, fail fast
- Never commit .env.local - add to .gitignore
- Commit .env.example - template for team
- Different per environment - dev, staging, production
What's Next?
You've mastered environment variables and configuration! Next, we'll explore Streaming and Suspense—using React Suspense for progressive rendering, streaming server-rendered content, implementing loading states with Suspense boundaries, and building performant, responsive UIs. You'll create fluid user experiences!
We'll cover Suspense boundaries, streaming SSR, loading patterns, and progressive enhancement.
🔐 Security Checklist
- ✅ Never commit .env.local to version control
- ✅ Never use NEXT_PUBLIC_ for secrets
- ✅ Validate all required variables on startup
- ✅ Use strong, unique secrets for each environment
- ✅ Rotate secrets regularly
- ✅ Document required variables in README
- ✅ Use environment-specific values