Effortlessly populate your environment at runtime, not just at build time.
β‘ Installation β’ π Getting Started β’ π Advanced Usage β’ π Deployment
Dynamic runtime environment variables for Next.js. This package is a Next.js 15/16 & React 19 compatible fork of next-runtime-env by Expatfile.tax LLC.
- Isomorphic Design: Works seamlessly on both server and browser, and even in middleware
- Next.js 15/16 & React 19 Ready: Fully compatible with the latest Next.js features including async server components
.envFriendly: Use.envfiles during development, just like standard Next.js- Type-Safe Parsers: Convert environment strings to booleans, numbers, arrays, JSON, URLs, and enums
- Secure by Default: XSS protection via JSON escaping, immutable runtime values with
Object.freeze - Zero Config: Works out of the box with sensible defaults
In the modern software development landscape, the Build once, deploy many philosophy is key. This principle, essential for easy deployment and testability, is a cornerstone of continuous delivery and is embraced by the twelve-factor methodology. However, front-end development, particularly with Next.js, often lacks support for this - requiring separate builds for different environments. next-dynenv bridges this gap.
next-dynenv dynamically injects environment variables into your Next.js application at runtime. This approach adheres to the "build once, deploy many" principle, allowing the same build to be used across various environments without rebuilds.
- next-dynenv@4.x: Next.js 15/16 & React 19 with modern async server components
Original project versions (unmaintained):
- next-runtime-env@3.x: Next.js 14 with advanced caching
- next-runtime-env@2.x: Next.js 13 App Router
- next-runtime-env@1.x: Next.js 12/13 Pages Router
npm install next-dynenv # or pnpm add next-dynenv # or yarn add next-dynenvIn your root layout (app/layout.tsx), add the PublicEnvScript component:
// app/layout.tsx import { PublicEnvScript } from 'next-dynenv' import type { ReactNode } from 'react' export default function RootLayout({ children }: { children: ReactNode }) { return ( <html lang="en"> <head> <PublicEnvScript /> </head> <body>{children}</body> </html> ) }The PublicEnvScript component automatically exposes all environment variables prefixed with NEXT_PUBLIC_ to the browser. For custom variable exposure, refer to the Exposing Custom Environment Variables guide.
// app/components/ClientComponent.tsx 'use client' import { env } from 'next-dynenv' export default function ClientComponent() { const apiUrl = env('NEXT_PUBLIC_API_URL') const debug = env('NEXT_PUBLIC_DEBUG_MODE') return ( <div> <p>API URL: {apiUrl}</p> <p>Debug Mode: {debug}</p> </div> ) }// app/components/ServerComponent.tsx import { env } from 'next-dynenv' export default async function ServerComponent() { // Server components in Next.js 15 can be async const apiUrl = env('NEXT_PUBLIC_API_URL') const secretKey = env('SECRET_API_KEY') // Server-side only variables also work return ( <div> <p>API URL: {apiUrl}</p> {/* Never expose secret keys to the client */} </div> ) }Note: In Next.js 15, server components can be async by default. The
env()function works seamlessly in both sync and async server components.
// middleware.ts import { env } from 'next-dynenv' import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' export function middleware(request: NextRequest) { // env() works in middleware too! const apiUrl = env('NEXT_PUBLIC_API_URL') const featureFlag = env('NEXT_PUBLIC_ENABLE_FEATURE') // Your middleware logic here if (featureFlag === 'true') { // Feature-specific logic } return NextResponse.next() } export const config = { matcher: '/api/:path*', }Note: The
env()function works in all Next.js contexts - server components, client components, API routes, and middleware. It's safe to use everywhere and provides a consistent API across your application.
The env() function accepts an optional default value:
import { env } from 'next-dynenv' // Returns 'https://api.default.com' if NEXT_PUBLIC_API_URL is undefined const apiUrl = env('NEXT_PUBLIC_API_URL', 'https://api.default.com') // Default values work in all contexts (client, server, middleware) const timeout = env('NEXT_PUBLIC_TIMEOUT', '5000')Use requireEnv() when a variable must be defined:
import { requireEnv } from 'next-dynenv' // Throws descriptive error if NEXT_PUBLIC_API_URL is undefined const apiUrl = requireEnv('NEXT_PUBLIC_API_URL') // Error: "Required environment variable 'NEXT_PUBLIC_API_URL' is not defined."Use serverOnly() for non-public environment variables in code that runs on both client and server. Unlike env(), this function never throws in the browserβit gracefully returns the fallback value:
import { serverOnly } from 'next-dynenv' // Returns the actual value on server, fallback on client const dbUrl = serverOnly('DATABASE_URL', 'postgresql://localhost:5432/dev') const apiSecret = serverOnly('API_SECRET_KEY') // undefined on clientThis is particularly useful for shared configuration modules with lazy evaluation:
import { env, serverOnly } from 'next-dynenv' import { z } from 'zod' const configSchema = z.object({ supabaseUrl: z.string().url(), supabaseAnonKey: z.string(), // Server-only: returns undefined on client, real value on server supabaseServiceKey: z.string().optional(), }) // Safe to import anywhereβevaluates lazily export const config = lazy(() => configSchema.parse({ supabaseUrl: env('NEXT_PUBLIC_SUPABASE_URL'), supabaseAnonKey: env('NEXT_PUBLIC_SUPABASE_ANON_KEY'), supabaseServiceKey: serverOnly('SUPABASE_SERVICE_KEY'), }), )Use envParsers to convert environment strings to typed values:
import { envParsers } from 'next-dynenv' // Boolean - recognizes 'true', '1', 'yes', 'on' (case-insensitive) const debug = envParsers.boolean('NEXT_PUBLIC_DEBUG') // false by default const enabled = envParsers.boolean('NEXT_PUBLIC_FEATURE', true) // custom default // Number - integers and floats const port = envParsers.number('NEXT_PUBLIC_PORT', 3000) const ratio = envParsers.number('NEXT_PUBLIC_RATIO', 1.0) // Array - comma-separated values (trims whitespace, filters empty) const features = envParsers.array('NEXT_PUBLIC_FEATURES') // 'auth, payments, analytics' β ['auth', 'payments', 'analytics'] // JSON - parse complex objects interface Config { api: string timeout: number } const config = envParsers.json<Config>('NEXT_PUBLIC_CONFIG') // URL - validates and returns URL string const apiUrl = envParsers.url('NEXT_PUBLIC_API_URL') const cdn = envParsers.url('NEXT_PUBLIC_CDN', 'https://cdn.default.com') // Enum - restrict to allowed values with type safety type Environment = 'development' | 'staging' | 'production' const appEnv = envParsers.enum<Environment>( 'NEXT_PUBLIC_ENV', ['development', 'staging', 'production'], 'development', // default value ) type LogLevel = 'debug' | 'info' | 'warn' | 'error' const logLevel = envParsers.enum<LogLevel>('NEXT_PUBLIC_LOG_LEVEL', ['debug', 'info', 'warn', 'error'])All parsers work isomorphically (server and client) and provide clear error messages for invalid values.
Need to expose environment variables without the NEXT_PUBLIC_ prefix? Check out the Making Environment Variables Public guide.
For fine-grained control over which variables are exposed to the browser, see the Exposing Custom Environment Variables guide.
When deploying with Docker, pass environment variables at runtime:
# Dockerfile FROM node:20-alpine AS base # Install dependencies FROM base AS deps WORKDIR /app COPY package*.json ./ RUN npm ci # Build application FROM base AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN npm run build # Production image FROM base AS runner WORKDIR /app ENV NODE_ENV=production COPY --from=builder /app/public ./public COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static EXPOSE 3000 ENV PORT=3000 CMD ["node", "server.js"]Run with environment variables:
docker run -p 3000:3000 \ -e NEXT_PUBLIC_API_URL=https://api.example.com \ -e NEXT_PUBLIC_APP_VERSION=1.0.0 \ your-app:latest- Add environment variables in your Vercel project settings
- Set different values for Preview, Development, and Production environments
- Deploy your application -
next-dynenvwill automatically use the runtime values
vercel env add NEXT_PUBLIC_API_URL production vercel env add NEXT_PUBLIC_API_URL previewAdd environment variables in your Netlify site settings:
- Go to Site settings β Environment variables
- Add your
NEXT_PUBLIC_*variables - Deploy contexts can have different values (Production, Deploy Previews, Branch deploys)
Configure environment variables in the Amplify Console:
- Navigate to App settings β Environment variables
- Add variables for each branch as needed
- Amplify will inject these at runtime during deployment
For static exports with runtime environment support:
- Build your application:
npm run build - Set environment variables on your hosting platform
- The variables will be available at runtime through
next-dynenv
This library includes multiple layers of security by default:
- XSS Protection: All environment values are JSON-escaped before injection, preventing script injection attacks
- Immutable Runtime Values: Environment values are wrapped with
Object.freeze(), preventing modification after initialization - Strict Prefix Enforcement: Only
NEXT_PUBLIC_*variables are exposed to the browser; accessing private variables throws an error
Critical: Only variables prefixed with NEXT_PUBLIC_ are exposed to the browser. Never expose sensitive data:
// β WRONG - Don't try to access secrets in client components 'use client' const apiKey = env('SECRET_API_KEY') // Throws error in browser! // β
CORRECT - Use secrets only server-side export async function getData() { const apiKey = env('SECRET_API_KEY') // Works in server components/API routes // ... fetch data securely }Use requireEnv() for required variables, or validate multiple at once:
// Using requireEnv() - throws if undefined import { requireEnv } from 'next-dynenv' const apiUrl = requireEnv('NEXT_PUBLIC_API_URL') // Validating multiple variables import { env } from 'next-dynenv' export function validateEnv() { const required = ['NEXT_PUBLIC_API_URL', 'NEXT_PUBLIC_APP_ID'] for (const key of required) { if (!env(key)) { throw new Error(`Missing required environment variable: ${key}`) } } }If using CSP, ensure inline scripts are allowed for the PublicEnvScript:
// next.config.js const ContentSecurityPolicy = ` script-src 'self' 'unsafe-inline'; ` module.exports = { async headers() { return [ { source: '/:path*', headers: [ { key: 'Content-Security-Policy', value: ContentSecurityPolicy.replace(/\s{2,}/g, ' ').trim(), }, ], }, ] }, }Problem: Environment variables return undefined in client components.
Solutions:
- Ensure variables have the
NEXT_PUBLIC_prefix - Verify
PublicEnvScriptis in your root layout's<head> - Check that variables are set in your environment (
.env.local, hosting platform, etc.) - Restart your development server after adding new environment variables
Problem: Changed environment variables don't reflect in deployed application.
Solutions:
- For Docker: Restart containers with new environment variables
- For Vercel/Netlify: Trigger a new deployment or redeploy
- Clear CDN cache if using one
- Verify variables are set in the correct deployment environment
Problem: TypeScript complains about env() return type.
Solution: The env() function returns string | undefined. Handle this explicitly:
const apiUrl = env('NEXT_PUBLIC_API_URL') ?? 'https://default-api.com' // Or with type assertion if you're certain it exists const apiUrl = env('NEXT_PUBLIC_API_URL')!Problem: Confusion about when variables are available.
Explanation:
- Build-time: Variables are baked into the bundle during
next build - Runtime: Variables are injected when the application starts
next-dynenvprovides runtime access, allowing the same build to work in multiple environments
Problem: Different behavior between server and client.
Key Differences:
-
Server-side contexts (server components, API routes, middleware):
- Can access ALL environment variables via
env()orprocess.env - Both private and public (
NEXT_PUBLIC_*) variables are available
- Can access ALL environment variables via
-
Client-side contexts (client components, browser):
- Can only access
NEXT_PUBLIC_*variables viaenv() - Private variables are not available for security reasons
- Attempting to access private variables throws an error
- Can only access
Recommendation: Use env() everywhere for consistency. It works in all contexts and provides better error messages when misused
This fork is maintained by Stefanie Jane (@hyperb1iss).
- Original Project: next-runtime-env by Expatfile.tax - All credit for the original implementation and core concepts
- Inspiration: react-env project
- Context Provider: Thanks to @andonirdgz for the innovative context provider idea
If you find this useful, buy me a Monster Ultra Violet β‘