Dynamic runtime environment variables for Next.js - populate your environment at runtime, not build time.
npm install next-dynenv






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
- .env Friendly: Use .env files 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
next-dynenv?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
``bash`
npm install next-dynenvor
pnpm add next-dynenvor
yarn add next-dynenv
In your root layout (app/layout.tsx), add the PublicEnvScript component:
`tsx
// app/layout.tsx
import { PublicEnvScript } from 'next-dynenv'
import type { ReactNode } from 'react'
export default function RootLayout({ children }: { children: ReactNode }) {
return (
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.$3
#### In Client Components
`tsx
// 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 (
API URL: {apiUrl}
Debug Mode: {debug}
)
}
`#### In Server Components (Next.js 15+)
`tsx
// 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 (
API URL: {apiUrl}
{/ Never expose secret keys to the client /}
)
}
`> Note: In Next.js 15, server components can be async by default. The
env() function works seamlessly in both sync
> and async server components.#### In Middleware
`tsx
// 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.$3
The
env() function accepts an optional default value:`tsx
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')
`$3
Use
requireEnv() when a variable must be defined:`tsx
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."
`$3
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:`tsx
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 client
`This is particularly useful for shared configuration modules with lazy evaluation:
`tsx
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'),
}),
)
`$3
Use
envParsers to convert environment strings to typed values:`tsx
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('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(
'NEXT_PUBLIC_ENV',
['development', 'staging', 'production'],
'development', // default value
)
type LogLevel = 'debug' | 'info' | 'warn' | 'error'
const logLevel = envParsers.enum('NEXT_PUBLIC_LOG_LEVEL', ['debug', 'info', 'warn', 'error'])
`All parsers work isomorphically (server and client) and provide clear error messages for invalid values.
π Advanced Usage
$3
Need to expose environment variables without the
NEXT_PUBLIC_ prefix? Check out the
Making Environment Variables Public guide.$3
For fine-grained control over which variables are exposed to the browser, see the
Exposing Custom Environment Variables guide.
π Deployment Guide
$3
When deploying with Docker, pass environment variables at runtime:
`dockerfile
Dockerfile
FROM node:20-alpine AS baseInstall dependencies
FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ciBuild application
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run buildProduction image
FROM base AS runner
WORKDIR /appENV 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:
`bash
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
`$3
1. Add environment variables in your Vercel project settings
2. Set different values for Preview, Development, and Production environments
3. Deploy your application -
next-dynenv will automatically use the runtime values`bash
vercel env add NEXT_PUBLIC_API_URL production
vercel env add NEXT_PUBLIC_API_URL preview
`$3
Add environment variables in your Netlify site settings:
1. Go to Site settings β Environment variables
2. Add your
NEXT_PUBLIC_* variables
3. Deploy contexts can have different values (Production, Deploy Previews, Branch deploys)$3
Configure environment variables in the Amplify Console:
1. Navigate to App settings β Environment variables
2. Add variables for each branch as needed
3. Amplify will inject these at runtime during deployment
$3
For static exports with runtime environment support:
1. Build your application:
npm run build
2. Set environment variables on your hosting platform
3. The variables will be available at runtime through next-dynenvπ Security Considerations
$3
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$3
Critical: Only variables prefixed with
NEXT_PUBLIC_ are exposed to the browser. Never expose sensitive data:`tsx
// β 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
}
`$3
Use
requireEnv() for required variables, or validate multiple at once:`tsx
// 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})
}
}
}
`$3
If using CSP, ensure inline scripts are allowed for the
PublicEnvScript:`tsx
// next.config.js
const ContentSecurityPolicy = module.exports = {
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'Content-Security-Policy',
value: ContentSecurityPolicy.replace(/\s{2,}/g, ' ').trim(),
},
],
},
]
},
}
`π Troubleshooting
$3
Problem: Environment variables return
undefined in client components.Solutions:
1. Ensure variables have the
NEXT_PUBLIC_ prefix
2. Verify PublicEnvScript is in your root layout's
3. Check that variables are set in your environment (.env.local, hosting platform, etc.)
4. Restart your development server after adding new environment variables$3
Problem: Changed environment variables don't reflect in deployed application.
Solutions:
1. For Docker: Restart containers with new environment variables
2. For Vercel/Netlify: Trigger a new deployment or redeploy
3. Clear CDN cache if using one
4. Verify variables are set in the correct deployment environment
$3
Problem: TypeScript complains about
env() return type.Solution: The
env() function returns string | undefined. Handle this explicitly:`tsx
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')!
`$3
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-dynenv provides runtime access, allowing the same build to work in multiple environments$3
Problem: Different behavior between server and client.
Key Differences:
- Server-side contexts (server components, API routes, middleware):
- Can access ALL environment variables via
env() or process.env
- Both private and public (NEXT_PUBLIC_*) variables are available- Client-side contexts (client components, browser):
- Can only access
NEXT_PUBLIC_* variables via env()
- Private variables are not available for security reasons
- Attempting to access private variables throws an errorRecommendation: Use
env()` everywhere for consistency. It works in all contexts and provides better error messages- Exposing Custom Environment Variables
- Making Environment Variables Public
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 β‘