Master how modern web applications consume configuration and secrets from their host environments. Learn the strict trust boundary separating private server-side variables from public client bundles, and discover why environment variables are not automatically secure without rigorous isolation.
THE CORE PRINCIPLE: Source code should never contain hardcoded secrets or environment-specific configuration. Inject configuration at runtime via the host environment, and treat any variable sent to the browser as completely public.
Decoupling dynamic configuration from executable code
An environment variable is a dynamic key-value pair provided to an application from the operating system or container runtime when the process boots, rather than being hardcoded directly into source files.
Non-sensitive operational settings that change between deployment targets.
• PORT=3000 or PORT=8080
• NODE_ENV=production
• LOG_LEVEL=debug
Sensitive credentials that grant administrative access to systems or cloud services.
• DATABASE_URL=postgres://...
• API_KEY=sk_live_948...
• JWT_SECRET=super-secret-key
Values safely exposed to the browser to direct frontend network traffic.
• NEXT_PUBLIC_API_URL
• NEXT_PUBLIC_ANALYTICS_ID
• NEXT_PUBLIC_APP_NAME
postgresql://dev:dev@localhost:5432/myapp. In production, it connects to a multi-region cloud cluster with SSL: postgresql://admin:StrongPass@db.prod.internal:5432/myapp?sslmode=require.process.env.DATABASE_URL, the exact same code executes cleanly in Development, Testing, Staging, and Production without changing a single line of code!Eliminating critical security vulnerabilities from source code
// paymentService.js
// ❌ WRONG: Secret baked directly into code
const STRIPE_KEY = "sk_live_9481948194819481948";
const DB_URL = "postgres://admin:Password123@prod.db:5432";
export async function processPayment(amount) {
const stripe = new Stripe(STRIPE_KEY);
return stripe.charges.create({ amount });
}// paymentService.js
// ✅ CORRECT: Injected from runtime host
const STRIPE_KEY = process.env.STRIPE_SECRET_KEY;
const DB_URL = process.env.DATABASE_URL;
export async function processPayment(amount) {
if (!STRIPE_KEY) {
throw new Error("FATAL: STRIPE_SECRET_KEY is missing!");
}
const stripe = new Stripe(STRIPE_KEY);
return stripe.charges.create({ amount });
}console.log(process.env) in an unauthenticated endpoint or expose them to client-side bundles, they leak immediately. Their safety depends entirely on where and how your application accesses them.The critical trust boundary in full stack frameworks like Next.js
In a Full Stack application, code runs in two completely different environments: on the Node.js Server and in the user's Web Browser.
Accessible ONLY in backend Node.js runtime (API Routes, Server Actions, Server Components).
// Server-Side Only: process.env.DATABASE_URL process.env.STRIPE_SECRET_KEY process.env.JWT_SECRET // Never sent across the network // Completely invisible to the browser!
Inlined into the JavaScript bundle at build time and delivered directly to the browser.
// Client-Side Accessible: process.env.NEXT_PUBLIC_API_URL process.env.NEXT_PUBLIC_ANALYTICS_ID // Inlined as string literal during build! // Anyone can view via DevTools -> Sources
NEXT_PUBLIC_ (or Vite's VITE_). When a framework compiler sees that prefix, it literally replaces the variable expression with the raw string literal. Anyone who opens browser DevTools can inspect it in plain text.Interactive full stack runtime simulator: Dev vs Prod
Simulate how a real full-stack web application reads configuration in Development vs Production. Modify values, toggle secret masking, and hit Apply Changes to inspect what the backend server receives versus what the browser bundle receives.
| Variable Key | Type | Runtime Value (development) | Action |
|---|---|---|---|
| APP_NAME Application display name | config | ||
| PORT Local server listening port | config | ||
| API_URL Backend service endpoint | config | ||
| DATABASE_URL Local Postgres connection string | secret | ||
| API_KEY Payment gateway test sandbox key | secret | ||
| NEXT_PUBLIC_ANALYTICS_ID Browser client telemetry identifier | client |
Local development workflows, templates, and native Node.js loading
# Local Developer Configuration PORT=3000 APP_NAME="Pathubs Local" # Local Real Secrets (DO NOT COMMIT!) DATABASE_URL=postgresql://alex:mypass@localhost:5432/mydb API_KEY=mock_stripe_test_51MzQ... JWT_SECRET=super_secret_dev_key_123
# Required Application Configuration PORT=3000 APP_NAME= # Database Connection (PostgreSQL) # Format: postgresql://[user]:[password]@[host]:[port]/[db] DATABASE_URL= # External API Keys (Get from dev portal) API_KEY= JWT_SECRET=
dotenv! Launch any script using native flags:node --env-file=.env server.js or node --env-file-if-exists=.env server.jsprocess.loadEnvFile('.env').Preventing credential leaks and executing the OWASP rotation protocol
# .gitignore # Ignore all local environment files .env .env.local .env.development.local .env.test.local .env.production.local # DO NOT ignore the template! !.env.example
# 1. Clone repository git clone https://github.com/org/repo.git # 2. Copy template to local file cp .env.example .env # 3. Fill in private local credentials # 4. Git ignores .env automatically!
Same application code + different environment configuration = different runtime behavior
NODE_ENV="development" PORT="3000" LOG_LEVEL="debug" API_URL="http://localhost:4000/api" DATABASE_URL="postgresql://dev:dev@localhost:5432/app" STRIPE_KEY="mock_sk_test_51...safe"
NODE_ENV="production" PORT="8080" LOG_LEVEL="warn" API_URL="https://api.pathubs.com/v1" DATABASE_URL="postgresql://prod:...@db-cluster.aws:5432/app?sslmode=require" STRIPE_KEY="mock_sk_live_88...safe"
Diagnose and fix typical full-stack environment traps
Evaluate 4 realistic production incidents. Select the correct engineering decision for each scenario to reveal the technical rationale.
// server.js
const dbUrl = process.env.DATABASE_URL;
const pool = new Pool({ connectionString: dbUrl });
// Application starts listening on PORT 3000...The application expects process.env.DATABASE_URL, but an engineer forgot to inject it into the production container. What should happen in a robust system?
// .env.production
NEXT_PUBLIC_STRIPE_SECRET_KEY=mock_sk_live_948194...safe
// src/components/CheckoutButton.tsx ('use client')
const stripeKey = process.env.NEXT_PUBLIC_STRIPE_SECRET_KEY;A developer prefixed their private Stripe secret key with NEXT_PUBLIC_ so their React client component could access it. Is this key still a secret?
commit 8f3b21c (HEAD -> main) Author: Dev <dev@company.com> Date: 3 days ago Added feature and included .env with DB credentials
A developer committed a .env file containing live database passwords to a public GitHub repository 3 days ago. What is the correct, safe remediation workflow?
// In production browser console: GET http://localhost:4000/api/users net::ERR_CONNECTION_REFUSED
A Next.js frontend deployed to production is failing because it tries to request http://localhost:4000. Which configuration issue is the cause?
Classify variable types, trust boundaries, and Git policies
Given a realistic full-stack application configuration, decide the Category,Runtime Destination, and Git Policy for each variable.
| Variable Name | Classification | Destination Boundary | Git Policy | Explanation |
|---|---|---|---|---|
APP_ENV | Complete row to evaluate | |||
DATABASE_URL | Complete row to evaluate | |||
API_URL | Complete row to evaluate | |||
API_KEY | Complete row to evaluate | |||
PORT | Complete row to evaluate |
.env is a local developer tool, NOT a security perimeter. Never commit it to Git.
Any variable prefixed with NEXT_PUBLIC_ is readable by anyone who opens DevTools.