Next.js course Β· Module 8: Deployment and Production
Environment variables and production configuration
In this lesson8
In Quantum Metropolis 2150, every system requires precise environmental configuration. Just as in the cyberpunk city every district has its own security protocols, a Next.js application needs different settings for different environments - development, staging, and production. Environment variables are the key to secure and flexible configuration management.
What are environment variables?
Environment variables are key-value pairs that configure the application's behavior without modifying the source code. Thanks to them, you can store:
- API keys and secrets (e.g., database keys, authorization tokens)
- URLs of external services
- Configuration flags (e.g., debug mode, logging level)
- Settings specific to the environment (development vs production)
.env files in Next.js
Next.js has built-in support for files .env. The framework automatically loads environment variables from several files, in a specified order of priority:
Hierarchy of .env files
1# .env files in Next.js (from the highest priority):
2.env.local # Local overrides - NEVER commit to git!
3.env.development # Only for npm run dev
4.env.production # Only for npm run build / npm start
5.env # Default values for all environmentsEach file can override values from files of lower priority. The file .env.local always has the highest priority and should be in .gitignore.
Creating .env files
1# .env - default values (committed to repo)
2NEXT_PUBLIC_APP_NAME=QuantumCity
3NEXT_PUBLIC_API_URL=http://localhost:4000/api
4DATABASE_URL=mongodb://localhost:27017/quantum_dev
5JWT_SECRET=development-secret-key-change-in-production
6
7# .env.local - local secrets (do NOT commit!)
8DATABASE_URL=mongodb://user:secret@prod-cluster.mongodb.net/quantum
9JWT_SECRET=super-secret-production-key-2150
10OPENAI_API_KEY=sk-proj-abc123...
11STRIPE_SECRET_KEY=sk_live_abc123...
12
13# .env.production - production settings
14NEXT_PUBLIC_API_URL=https://api.quantum-city.com
15NEXT_PUBLIC_APP_NAME=QuantumCity Production
16NODE_ENV=productionPrefix NEXT_PUBLIC_ - the key to security
This is one of the most important rules in Next.js! The prefix NEXT_PUBLIC_ determines whether the variable is available in the browser:
1// Variables WITHOUT prefix - available ONLY on the server
2// They never make it into the browser bundle!
3const dbUrl = process.env.DATABASE_URL; // OK in Server Components
4const apiKey = process.env.OPENAI_API_KEY; // OK in Route Handlers
5const secret = process.env.JWT_SECRET; // OK in middleware
6
7// Variables WITH the NEXT_PUBLIC_ prefix - available EVERYWHERE
8// They make it into the JavaScript bundle in the browser!
9const appName = process.env.NEXT_PUBLIC_APP_NAME; // OK in Client Components
10const apiUrl = process.env.NEXT_PUBLIC_API_URL; // OK everywhereExample of usage in components
1// app/layout.tsx - Server Component
2export default function RootLayout({ children }: { children: React.ReactNode }) {
3 // Safe - server variable, does not reach the browser
4 const dbUrl = process.env.DATABASE_URL;
5 console.log('Connecting to:', dbUrl);
6
7 return (
8 <html>
9 <body>
10 {/* NEXT_PUBLIC_ - safely used in HTML */}
11 <h1>{process.env.NEXT_PUBLIC_APP_NAME}</h1>
12 {children}
13 </body>
14 </html>
15 );
16}
17
18// components/ApiStatus.tsx - Client Component
19'use client';
20import { useEffect, useState } from 'react';
21
22export default function ApiStatus() {
23 const [status, setStatus] = useState('checking...');
24
25 useEffect(() => {
26 // NEXT_PUBLIC_ variables work in the browser
27 const apiUrl = process.env.NEXT_PUBLIC_API_URL;
28
29 fetch(apiUrl + '/health')
30 .then(res => res.json())
31 .then(data => setStatus(data.status));
32 }, []);
33
34 return <span>API: {status}</span>;
35}Runtime vs Build-time environment variables
It's important to understand when variables are read:
Build-time
Variables NEXT_PUBLIC_ are injected during npm run build. This means that after building the application, their values are "frozen" in the JavaScript code:
1# These values will be injected into the bundle during build:
2NEXT_PUBLIC_API_URL=https://api.quantum-city.com npm run build
3
4# After building, changing this variable will NOT change the application's behavior!
5# You need to rebuild to apply a new value.Runtime
Server variables (without NEXT_PUBLIC_) are read in real-time - at every request:
1// app/api/config/route.ts
2import { NextResponse } from 'next/server';
3
4export async function GET() {
5 // This value is read AT EVERY request
6 // Changing the variable on the server immediately affects the response
7 const dbUrl = process.env.DATABASE_URL;
8 const maxConnections = process.env.DB_MAX_CONNECTIONS || '10';
9
10 return NextResponse.json({
11 database: dbUrl ? 'connected' : 'not configured',
12 maxConnections: parseInt(maxConnections),
13 });
14}Validation of environment variables with Zod (t3-env pattern)
In professional applications, it's worth validating environment variables at application startup to avoid runtime errors. A popular approach is to use the Zod library:
1// lib/env.ts
2import { z } from 'zod';
3
4// Validation schema for server variables
5const serverEnvSchema = z.object({
6 DATABASE_URL: z.string().url('DATABASE_URL must be a valid URL'),
7 JWT_SECRET: z.string().min(32, 'JWT_SECRET must have at least 32 characters'),
8 OPENAI_API_KEY: z.string().startsWith('sk-', 'Invalid OpenAI key'),
9 NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
10 PORT: z.coerce.number().default(4000),
11});
12
13// Validation schema for client variables
14const clientEnvSchema = z.object({
15 NEXT_PUBLIC_APP_NAME: z.string().min(1),
16 NEXT_PUBLIC_API_URL: z.string().url(),
17});
18
19// Validation at module import
20const serverEnv = serverEnvSchema.safeParse(process.env);
21const clientEnv = clientEnvSchema.safeParse(process.env);
22
23if (!serverEnv.success) {
24 console.error('Missing server variables:', serverEnv.error.format());
25 throw new Error('Invalid server environment configuration');
26}
27
28if (!clientEnv.success) {
29 console.error('Missing client variables:', clientEnv.error.format());
30 throw new Error('Invalid client environment configuration');
31}
32
33export const env = {
34 ...serverEnv.data,
35 ...clientEnv.data,
36};Using validated variables
1// app/api/users/route.ts
2import { env } from '@/lib/env';
3
4export async function GET() {
5 // TypeScript knows the types - env.DATABASE_URL is always a string
6 // Validation guarantees that the value is a valid URL
7 const response = await fetch(env.NEXT_PUBLIC_API_URL + '/users', {
8 headers: {
9 'Authorization': 'Bearer ' + env.JWT_SECRET,
10 },
11 });
12
13 return Response.json(await response.json());
14}next.config.js configuration for different environments
The next.config.js file can use environment variables for dynamic configuration:
1// next.config.js
2/** @type {import('next').NextConfig} */
3const nextConfig = {
4 // Setting additional environment variables
5 env: {
6 CUSTOM_KEY: process.env.NODE_ENV === 'production'
7 ? 'production-value'
8 : 'development-value',
9 BUILD_TIME: new Date().toISOString(),
10 },
11
12 // Environment-dependent image configuration
13 images: {
14 remotePatterns: [
15 {
16 protocol: 'https',
17 hostname: process.env.NODE_ENV === 'production'
18 ? 'cdn.quantum-city.com'
19 : 'localhost',
20 },
21 ],
22 },
23
24 // Different headers for dev and prod
25 async headers() {
26 const securityHeaders = [
27 { key: 'X-Frame-Options', value: 'SAMEORIGIN' },
28 { key: 'X-Content-Type-Options', value: 'nosniff' },
29 ];
30
31 if (process.env.NODE_ENV === 'production') {
32 securityHeaders.push({
33 key: 'Strict-Transport-Security',
34 value: 'max-age=63072000; includeSubDomains; preload',
35 });
36 }
37
38 return [{ source: '/:path*', headers: securityHeaders }];
39 },
40};
41
42module.exports = nextConfig;Managing secrets in production
In the production environment, we never store secrets in files .env. Instead, we use platforms for secrets management:
1# Vercel - adding variables via the CLI
2vercel env add DATABASE_URL production
3vercel env add JWT_SECRET production
4vercel env add OPENAI_API_KEY production
5
6# Vercel - variables preview
7vercel env ls
8
9# Docker - variables via docker-compose.yml
10# docker-compose.yml:
11# services:
12# app:
13# environment:
14# - DATABASE_URL=${DATABASE_URL}
15# - JWT_SECRET=${JWT_SECRET}
16
17# GitHub Actions - secrets in CI/CD
18# Settings β Secrets and variables β Actions
19# Usage: ${{ secrets.DATABASE_URL }}Summary
Environment variables are the foundation of secure Next.js application configuration:
- .env files - hierarchy:
.env.local>.env.production>.env - NEXT_PUBLIC_ prefix - only these variables reach the browser
- Runtime vs Build-time - server variables are dynamic, client variables are frozen after build
- Validation with Zod - guarantees configuration correctness
- Secrets in production - never in files, always through the hosting platform
Practice these concepts in the editor below.
Code for this lesson: App.tsx
1import React, { useState } from 'react';
2
3// Demo: Environment variables and production configuration
4interface EnvVar {
5 name: string;
6 value: string;
7 type: 'server' | 'public';
8 file: string;
9 description: string;
10}
11
12const envVars: EnvVar[] = [
13 { name: 'DATABASE_URL', value: 'mongodb://localhost:27017/quantum', type: 'server', file: '.env', description: 'Database URL' },
14 { name: 'JWT_SECRET', value: 'dev-secret-key-change-me', type: 'server', file: '.env.local', description: 'JWT key (secret!)' },
15 { name: 'OPENAI_API_KEY', value: 'sk-proj-abc123...', type: 'server', file: '.env.local', description: 'OpenAI API key' },
16 { name: 'NEXT_PUBLIC_APP_NAME', value: 'QuantumCity', type: 'public', file: '.env', description: 'App name (visible in browser)' },
17 { name: 'NEXT_PUBLIC_API_URL', value: 'http://localhost:4000/api', type: 'public', file: '.env', description: 'API URL (visible in browser)' },
18 { name: 'NODE_ENV', value: 'development', type: 'server', file: 'system', description: 'Runtime environment' },
19];
20
21type EnvFile = '.env' | '.env.local' | '.env.production' | '.env.development';
22
23const fileHierarchy: { file: EnvFile; priority: number; description: string; gitIgnored: boolean }[] = [
24 { file: '.env.local', priority: 1, description: 'Local overrides (highest priority)', gitIgnored: true },
25 { file: '.env.development', priority: 2, description: 'Only for npm run dev', gitIgnored: false },
26 { file: '.env.production', priority: 2, description: 'Only for npm run build/start', gitIgnored: false },
27 { file: '.env', priority: 3, description: 'Default values', gitIgnored: false },
28];
29
30export default function EnvVarsDemo() {
31 const [activeTab, setActiveTab] = useState<'vars' | 'hierarchy' | 'validation'>('vars');
32 const [filter, setFilter] = useState<'all' | 'server' | 'public'>('all');
33 const [showValues, setShowValues] = useState(false);
34
35 const filtered = filter === 'all' ? envVars : envVars.filter(v => v.type === filter);
36
37 const zodSchema = `import { z } from 'zod';
38
39const serverEnvSchema = z.object({
40 DATABASE_URL: z.string().url(),
41 JWT_SECRET: z.string().min(32),
42 OPENAI_API_KEY: z.string().startsWith('sk-'),
43 NODE_ENV: z.enum(['development', 'production', 'test']),
44});
45
46const clientEnvSchema = z.object({
47 NEXT_PUBLIC_APP_NAME: z.string().min(1),
48 NEXT_PUBLIC_API_URL: z.string().url(),
49});
50
51// Validation at application startup
52const result = serverEnvSchema.safeParse(process.env);
53if (!result.success) {
54 throw new Error('Missing variables!');
55}`;
56
57 return (
58 <div style={{ background: '#0f0f23', minHeight: '100vh', padding: '20px', color: '#e0e0e0', fontFamily: 'system-ui' }}>
59 <h1 style={{ color: '#64ffda', marginBottom: '8px' }}>Environment Variables</h1>
60 <p style={{ color: '#8892b0', marginBottom: '24px' }}>Next.js production configuration</p>
61
62 <div style={{ display: 'flex', gap: '8px', marginBottom: '24px' }}>
63 {[
64 { id: 'vars' as const, label: 'Variables' },
65 { id: 'hierarchy' as const, label: 'Hierarchia .env' },
66 { id: 'validation' as const, label: 'Zod Validation' },
67 ].map(t => (
68 <button key={t.id} onClick={() => setActiveTab(t.id)} style={{
69 padding: '8px 16px', borderRadius: '8px', border: 'none', cursor: 'pointer',
70 background: activeTab === t.id ? '#64ffda' : '#1a1a2e', color: activeTab === t.id ? '#0f0f23' : '#8892b0', fontWeight: 'bold',
71 }}>{t.label}</button>
72 ))}
73 </div>
74
75 {activeTab === 'vars' && (
76 <div>
77 <div style={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>
78 {(['all', 'server', 'public'] as const).map(f => (
79 <button key={f} onClick={() => setFilter(f)} style={{
80 padding: '6px 12px', borderRadius: '6px', border: 'none', cursor: 'pointer', fontSize: '13px',
81 background: filter === f ? '#7c3aed' : '#1a1a2e', color: filter === f ? '#fff' : '#8892b0',
82 }}>{f === 'all' ? 'All' : f === 'server' ? 'Server-side' : 'NEXT_PUBLIC_'}</button>
83 ))}
84 <button onClick={() => setShowValues(!showValues)} style={{
85 padding: '6px 12px', borderRadius: '6px', border: '1px solid #333', background: 'transparent', color: '#8892b0', cursor: 'pointer', fontSize: '13px', marginLeft: 'auto',
86 }}>{showValues ? 'Hide values' : 'Show values'}</button>
87 </div>
88 <div style={{ display: 'grid', gap: '8px' }}>
89 {filtered.map(v => (
90 <div key={v.name} style={{
91 padding: '12px', borderRadius: '8px', background: '#1a1a2e',
92 border: '1px solid ' + (v.type === 'server' ? '#f4433640' : '#64ffda40'),
93 }}>
94 <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '4px' }}>
95 <code style={{ color: v.type === 'server' ? '#f44336' : '#64ffda', fontWeight: 'bold', fontSize: '13px' }}>{v.name}</code>
96 <span style={{ fontSize: '10px', padding: '2px 6px', borderRadius: '4px', background: v.type === 'server' ? '#f4433620' : '#64ffda20', color: v.type === 'server' ? '#f44336' : '#64ffda' }}>
97 {v.type === 'server' ? 'ONLY SERVER' : 'PUBLIC (browser)'}
98 </span>
99 <span style={{ fontSize: '10px', color: '#666', marginLeft: 'auto' }}>{v.file}</span>
100 </div>
101 <p style={{ color: '#8892b0', fontSize: '12px', margin: '4px 0 0' }}>{v.description}</p>
102 {showValues && <code style={{ color: '#ff9800', fontSize: '12px', display: 'block', marginTop: '4px' }}>{v.value}</code>}
103 </div>
104 ))}
105 </div>
106 <div style={{ marginTop: '16px', padding: '12px', borderRadius: '8px', background: '#f4433610', border: '1px solid #f4433630' }}>
107 <p style={{ color: '#f44336', fontSize: '13px', margin: 0, fontWeight: 'bold' }}>Warning!</p>
108 <p style={{ color: '#8892b0', fontSize: '12px', margin: '4px 0 0' }}>Variables WITHOUT the NEXT_PUBLIC_ prefix are available ONLY on the server. They never reach the browser!</p>
109 </div>
110 </div>
111 )}
112
113 {activeTab === 'hierarchy' && (
114 <div>
115 <h3 style={{ color: '#64ffda', fontSize: '16px', marginBottom: '12px' }}>Hierarchy of .env files (from highest priority)</h3>
116 {fileHierarchy.map((f, i) => (
117 <div key={f.file} style={{
118 padding: '16px', marginBottom: '8px', borderRadius: '8px',
119 background: '#1a1a2e', border: '1px solid ' + (i === 0 ? '#64ffda40' : '#33333380'),
120 display: 'flex', alignItems: 'center', gap: '16px',
121 }}>
122 <span style={{ color: '#64ffda', fontSize: '24px', fontWeight: 'bold', width: '30px' }}>{f.priority}</span>
123 <div style={{ flex: 1 }}>
124 <code style={{ color: '#fff', fontWeight: 'bold' }}>{f.file}</code>
125 <p style={{ color: '#8892b0', fontSize: '12px', margin: '4px 0 0' }}>{f.description}</p>
126 </div>
127 <span style={{ fontSize: '11px', padding: '2px 8px', borderRadius: '4px', background: f.gitIgnored ? '#f4433620' : '#4caf5020', color: f.gitIgnored ? '#f44336' : '#4caf50' }}>
128 {f.gitIgnored ? '.gitignore' : 'tracked'}
129 </span>
130 </div>
131 ))}
132 </div>
133 )}
134
135 {activeTab === 'validation' && (
136 <div>
137 <h3 style={{ color: '#64ffda', fontSize: '16px', marginBottom: '12px' }}>Validation with Zod (t3-env pattern)</h3>
138 <pre style={{ background: '#1a1a2e', padding: '16px', borderRadius: '8px', fontSize: '12px', overflow: 'auto', border: '1px solid #333', whiteSpace: 'pre-wrap' }}>
139 {zodSchema}
140 </pre>
141 <p style={{ color: '#8892b0', fontSize: '13px', marginTop: '12px' }}>Validating variables at application startup prevents runtime errors.</p>
142 </div>
143 )}
144 </div>
145 );
146}Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What does the NEXT_PUBLIC_ prefix mean for environment variables in Next.js?
2. Which .env file has the HIGHEST priority in Next.js?
These are 2 of 3 questions for this lesson. Solve the rest in the game.