Environment Variables in Full Stack Web Development
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.
01. What Are Environment Variables?
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
In development, your backend connects to a local container:
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.Because the application reads
process.env.DATABASE_URL, the exact same code executes cleanly in Development, Testing, Staging, and Production without changing a single line of code!02. Environment Variables vs Hardcoding
Eliminating critical security vulnerabilities from source code
Hardcoded in Source Code (❌ Dangerous)
// 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 });
}Injected via Environment Variable (✅ Secure)
// 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 });
}Environment variables are an injection mechanism, not an encryption vault. If you print
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.03. Server-Side vs Client-Side Variables
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.
Server-Side Variables
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!
Client-Side Variables (NEXT_PUBLIC_)
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
NEVER prefix a private credential with
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.🔥 04. Live Environment Variable Playground
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 |
05. .env and .env.example Files
Local development workflows, templates, and native Node.js loading
.env (Local Machine Only — Ignored by Git)
# 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
.env.example (Committed to Git Repository)
# 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=
Starting with Node.js v20.6.0+, you no longer need third-party packages like
dotenv! Launch any script using native flags:node --env-file=.env server.js or node --env-file-if-exists=.env server.jsProgrammatic access is also built-in:
process.loadEnvFile('.env').06. Git + Secrets: The Fatal Mistake
Preventing credential leaks and executing the OWASP rotation protocol
.gitignore Configuration
# .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
Safe Team Onboarding Flow
# 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!
Do NOT simply delete the file or make a new commit!
Automated scraper bots monitor public commits in milliseconds. Deleting the file leaves the credential accessible in Git history.
MANDATORY REMEDIATION:
1. ROTATE / REVOKE IMMEDIATELY: Revoke the key immediately at the provider (AWS, Stripe, Database).
2. ISSUE A NEW SECRET: Generate a fresh key and inject it through secure container/cloud secret managers.
3. AUDIT ACCESS LOGS: Check whether unauthorized activity occurred during the exposure window.
07. Development vs Production
Same application code + different environment configuration = different runtime behavior
Development Environment
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"
- Fast local iteration with Hot Module Replacement
- Verbose debug logging enabled
- Safe sandbox credentials with zero financial impact
Production Environment
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"
- High performance optimizations with minified code
- Strict TLS/SSL database connections
- Live credentials with restricted IP allowlists
🔥 08. Real-World Debugging Lab
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.
Scenario 1: Missing DATABASE_URL in Production
// 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?
Scenario 2: Secret Exposed via NEXT_PUBLIC_
// .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?
Scenario 3: .env with Live Database Password Committed to Git
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?
Scenario 4: Production App Fetching http://localhost:4000
// 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?
🎯 09. Final Mini Challenge: Variable Matrix
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 Local Convenience
.env is a local developer tool, NOT a security perimeter. Never commit it to Git.
Client Variables Are Public
Any variable prefixed with NEXT_PUBLIC_ is readable by anyone who opens DevTools.