Environment Variables & Configuration in Next.js

1. Introduction ๐Ÿš€

Every real application needs a way to change behavior between development, preview, and production without changing code. Next.js handles this through environment variables and the next.config.ts file. This tutorial covers both โ€” how to load secrets safely, and how to shape the framework's build and runtime behavior.

Information

Getting the public vs. server-only distinction right is the single most important thing in this whole topic โ€” get it wrong, and secrets can leak into the browser bundle.

2. What are Environment Variables? ๐Ÿค”

An environment variable is a named value available to your running code but stored outside of it โ€” in a file or your hosting provider's dashboard โ€” so the same codebase can behave differently across environments.

  • Database connection strings, API keys, feature flags.
  • Accessed in code via process.env.VARIABLE_NAME.
  • Never hardcoded directly into source files.

3. Why Use Environment Variables? ๐ŸŽฏ

Beyond convenience, environment variables keep secrets out of source control and let the exact same build artifact be deployed to different environments with different configuration.

Tip

A good rule of thumb: if a value would be embarrassing or dangerous to see in a public GitHub repo, it belongs in an environment variable โ€” not in code.

4. Environment Files ๐Ÿ“

Next.js automatically loads variables from several .env file variants, each serving a different purpose.

my-app
.env
.env.local
.env.development
.env.production
.env.test
FileLoaded whenShould be committed?
.envAlwaysYes (no secrets)
.env.localAlways (except test)Never
.env.developmentnext devYes (no secrets)
.env.productionnext build / next startYes (no secrets)

5. Loading Environment Variables ๐Ÿ“ฅ

Next.js loads these files automatically โ€” no extra package like dotenv is required. Values become available on process.env as soon as the process starts.

.env.local

DATABASE_URL="postgresql://user:pass@localhost:5432/mydb"
STRIPE_SECRET_KEY="sk_live_xxx"
NEXT_PUBLIC_ANALYTICS_ID="G-XXXXXXX"

6. Public Environment Variables ๐ŸŒ

Any variable prefixed with NEXT_PUBLIC_ is inlined into the client-side JavaScript bundle at build time, making it accessible in the browser.

components/Analytics.tsx

'use client';

export function Analytics() {
  const id = process.env.NEXT_PUBLIC_ANALYTICS_ID;
  return <script data-analytics-id={id} />;
}

Warning

Never put secrets โ€” API keys, tokens, database credentials โ€” behind a NEXT_PUBLIC_ prefix. Anything with that prefix should be treated as publicly visible.

7. Server-Only Environment Variables ๐Ÿ”’

Variables without the NEXT_PUBLIC_ prefix are only available in server-side code โ€” Server Components, Route Handlers, Middleware, and Server Actions โ€” and are stripped from the client bundle.

app/api/checkout/route.ts

export async function POST(request: Request) {
  const stripeKey = process.env.STRIPE_SECRET_KEY; // safe: server-only
  const client = new Stripe(stripeKey!);
  // ...
}

8. process.env ๐Ÿงพ

process.env is the standard Node.js object through which all environment variables are read, regardless of framework. Next.js replaces references at build time for statically-known keys, enabling dead-code elimination.

lib/config.ts

export const isProduction = process.env.NODE_ENV === 'production';
export const apiUrl = process.env.API_URL ?? 'http://localhost:3000';

Caution

Destructuring process.env dynamically (e.g. const { [key]: value } = process.env) can break Next.js's build-time inlining โ€” always reference variables directly as process.env.MY_VAR.

9. Environment Variable Expansion ๐Ÿ”—

Next.js supports referencing one variable's value from another using $VARIABLE_NAME syntax within a .env file.

.env

BASE_URL="https://api.example.com"
USERS_ENDPOINT="$BASE_URL/users"

10. Environment Variable Load Order ๐Ÿ“Š

When the same variable is defined in multiple files, Next.js resolves it using a fixed precedence order, checking each file in turn until a value is found.

  1. process.env (already set in the shell/host)
  2. .env.$(NODE_ENV).local
  3. .env.local (skipped in test)
  4. .env.$(NODE_ENV)
  5. .env

11. Development Configuration ๐Ÿ› ๏ธ

Use .env.development (or .env.local) for values specific to local development, like a local database URL or a test-mode payment key.

.env.development

DATABASE_URL="postgresql://localhost:5432/dev_db"
STRIPE_SECRET_KEY="sk_test_xxx"

12. Production Configuration ๐Ÿญ

Production secrets are almost always set through your hosting provider's dashboard (Vercel, for example) rather than committed to .env.production, since that file is version-controlled.

Best Practice

Treat .env.production as a place for non-secret production defaults only โ€” real secrets belong in your hosting provider's encrypted environment variable storage.

13. Preview Configuration ๐Ÿ‘๏ธ

Many hosting providers support a separate preview environment for pull requests, letting you use a staging database or sandboxed API keys distinct from both development and production.

EnvironmentTypical Use
DevelopmentLocal machine, hot reload
PreviewPull request deployments, staging data
ProductionLive traffic, real secrets

14. next.config.ts โš™๏ธ

next.config.ts (or .js) is where you configure framework-level behavior โ€” build output, redirects, image domains, experimental flags โ€” as opposed to runtime secrets.

next.config.ts

import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  reactStrictMode: true,
  images: { domains: ['cdn.example.com'] },
};

export default nextConfig;

15. Application Configuration ๐Ÿงฉ

Beyond next.config.ts, most apps keep a small, typed configuration module that centralizes reads from process.env, so the rest of the codebase never touches process.env directly.

lib/config.ts

export const config = {
  databaseUrl: process.env.DATABASE_URL!,
  stripeKey: process.env.STRIPE_SECRET_KEY!,
  siteUrl: process.env.NEXT_PUBLIC_SITE_URL ?? 'http://localhost:3000',
};

16. Runtime Configuration ๐Ÿ”„

Unlike build-time inlined NEXT_PUBLIC_ variables, true runtime configuration is read fresh every time the server process starts, letting the same build be deployed to multiple environments unchanged.

Information

Server-only environment variables are naturally runtime configuration โ€” they're read directly by the running Node.js/Edge process rather than baked into the client bundle.

17. Build Configuration ๐Ÿ—๏ธ

Some next.config.ts options directly affect the build process itself โ€” like output mode, TypeScript/ESLint error handling, or compiler options.

next.config.ts

const nextConfig: NextConfig = {
  typescript: { ignoreBuildErrors: false },
  eslint: { ignoreDuringBuilds: false },
};

18. Experimental Features ๐Ÿงช

The experimental key in next.config.ts gates in-progress features that may change or be removed between releases โ€” useful for early adoption but riskier for production stability.

next.config.ts

const nextConfig: NextConfig = {
  experimental: {
    ppr: true,
    typedRoutes: true,
  },
};

Caution

Experimental flags can change behavior or disappear entirely between minor versions โ€” pin your Next.js version carefully if you rely on them in production.

19. Turbopack Configuration โšก

Turbopack is Next.js's Rust-based bundler, configurable through the turbopack key for custom loaders, resolve aliases, and module rules.

next.config.ts

const nextConfig: NextConfig = {
  turbopack: {
    resolveAlias: {
      '@ui': './src/components/ui',
    },
  },
};

20. Webpack Configuration ๐Ÿ“ฆ

For projects not yet on Turbopack, the webpack() function lets you extend or override the underlying webpack configuration directly.

next.config.ts

const nextConfig: NextConfig = {
  webpack(config) {
    config.resolve.alias['@ui'] = './src/components/ui';
    return config;
  },
};

21. Redirects Configuration โ†ช๏ธ

The redirects() function lets you define permanent or temporary URL redirects centrally, without writing Middleware or per-page logic.

next.config.ts

const nextConfig: NextConfig = {
  async redirects() {
    return [
      { source: '/old-blog/:slug', destination: '/blog/:slug', permanent: true },
    ];
  },
};

22. Rewrites Configuration ๐Ÿ”€

The rewrites() function maps an incoming path to a different destination without changing the URL the visitor sees โ€” useful for proxying an external API.

next.config.ts

const nextConfig: NextConfig = {
  async rewrites() {
    return [
      { source: '/api/proxy/:path*', destination: 'https://external-api.com/:path*' },
    ];
  },
};

23. Headers Configuration ๐Ÿท๏ธ

The headers() function attaches custom response headers to matched paths at the framework level, ideal for global security or caching headers.

next.config.ts

const nextConfig: NextConfig = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [{ key: 'X-Frame-Options', value: 'DENY' }],
      },
    ];
  },
};

24. Image Configuration ๐Ÿ–ผ๏ธ

The images key configures which external domains the built-in next/image component is allowed to optimize, along with formats and device sizes.

next.config.ts

const nextConfig: NextConfig = {
  images: {
    remotePatterns: [{ protocol: 'https', hostname: 'cdn.example.com' }],
    formats: ['image/avif', 'image/webp'],
  },
};

25. Transpile Packages ๐Ÿ“ฆ

The transpilePackages option tells Next.js to compile specific node_modules packages through its own build pipeline โ€” necessary for monorepo packages or libraries shipped as untranspiled ESM/TS.

next.config.ts

const nextConfig: NextConfig = {
  transpilePackages: ['ui', '@my-org/shared-components'],
};

26. Output Configuration ๐Ÿ“ค

The output option changes what next build produces โ€” the default full server build, a minimal standalone bundle for containers, or a fully exported static site.

next.config.ts

const nextConfig: NextConfig = {
  output: 'standalone', // ideal for Docker deployments
};
ValueUse Case
(default)Standard Node.js server deployment
standaloneMinimal, self-contained Docker image
exportFully static site, no server required

27. Security Best Practices ๐Ÿ”’

  • Never prefix secrets with NEXT_PUBLIC_ โ€” that inlines them into publicly readable JavaScript.
  • Add .env*.local to .gitignore so local secrets never reach version control.
  • Store production secrets in your hosting provider's encrypted environment variable manager, not in a committed file.
  • Rotate leaked keys immediately and audit git history if a secret was ever committed by mistake.

Danger

A NEXT_PUBLIC_-prefixed secret is permanently baked into every historical build's JavaScript bundle โ€” removing the prefix later does not retroactively secure old deployments.

28. Common Configuration Mistakes ๐Ÿšซ

Common Environment & Config Mistakes
Accidentally prefixing a secret key with NEXT_PUBLIC_
Committing .env.local to version control
Build-time inlining confusion
Forgetting to set required variables in the preview or production dashboard, causing a broken deploy
Destructuring process.env dynamically, breaking static replacement
Expecting a non-NEXT_PUBLIC_ variable to be readable in a Client Component

29. Frequently Asked Questions โ“

Yes โ€” environment files are only read when the Next.js process starts, so any change to a .env file requires restarting next dev.

Yes โ€” recent versions of Next.js support next.config.ts directly, giving you full type-checking on configuration options.

No โ€” Next.js sets NODE_ENV automatically based on the command you run (development for next dev, production for next build/next start); you shouldn't override it manually.

30. Summary ๐Ÿ“š