We use cookies to enhance your experience on the site
CodeWorlds

Environment variables and production configuration

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 environments

Each 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=production

Prefix 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 everywhere

Example 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:

  1. .env files — hierarchy:
    .env.local
    >
    .env.production
    >
    .env
  2. NEXT_PUBLIC_ prefix — only these variables reach the browser
  3. Runtime vs Build-time — server variables are dynamic, client variables are frozen after build
  4. Validation with Zod — guarantees configuration correctness
  5. Secrets in production — never in files, always through the hosting platform

Practice these concepts in the editor below.

Go to CodeWorlds