Frontend Environment Variables Guide
This guide explains how environment variables work in the Next.js frontend, from local development to production deployment.
Overview
Frontend environment variables in Next.js work differently than backend variables. They are processed at build time and embedded into the compiled application. This means they need to be available during the Docker build process.
Types of Environment Variables
1. Public Variables (NEXT_PUBLIC_*)
- Available: Browser and server
- Security: Exposed to client-side code
- Usage: Public configuration that’s safe to expose
// Available in browser JavaScript
const apiUrl = process.env.NEXT_PUBLIC_BACKEND_URL;
2. Server-Only Variables
- Available: Server-side only (API routes, server components)
- Security: Hidden from client-side code
- Usage: Secrets, API keys, server configuration
Examples:
MAILGUN_WEBHOOK_SECRET- Webhook signature validation (required for email webhooks)LANGFUSE_SECRET_KEY- Analytics/tracing secret (NOT used in frontend - only backend needs this)- Any sensitive API keys or secrets
// Only available in API routes and server components
const webhookSecret = process.env.MAILGUN_WEBHOOK_SECRET;
const langfuseSecret = process.env.LANGFUSE_SECRET_KEY;
Environment Variable Processing Flow
Local Development
.env.local → Next.js dev server → Available at runtime
Production Build
Secret Manager → get-firebase-config.sh → Docker build args → Embedded in build → Available at runtime
Configuration Files
1. .env.local (Local Development)
# Public variables (exposed to browser - safe to expose)
NEXT_PUBLIC_FIREBASE_API_KEY=your-key
NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY=pk-your-public-key
NEXT_PUBLIC_BACKEND_URL=http://127.0.0.1:1956
# Server-only variables (API routes only - KEEP SECRET!)
MAILGUN_WEBHOOK_SECRET=your-webhook-secret
LANGFUSE_SECRET_KEY=sk-your-secret-key # Note: Not actually used in frontend
2. next.config.mjs (Next.js Configuration)
Declares which environment variables are available:
env: {
// Public variables only (NOTE: These are exposed to browser!)
NEXT_PUBLIC_BACKEND_URL: process.env.NEXT_PUBLIC_BACKEND_URL,
NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY: process.env.NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY
// IMPORTANT: Secrets are NOT included here
},
serverRuntimeConfig: {
// Server-only variables (NEVER exposed to browser)
MAILGUN_WEBHOOK_SECRET: process.env.MAILGUN_WEBHOOK_SECRET,
LANGFUSE_SECRET_KEY: process.env.LANGFUSE_SECRET_KEY // Note: Not used in frontend
},
publicRuntimeConfig: {
// Client-accessible variables (alternative to NEXT_PUBLIC_)
NEXT_PUBLIC_BACKEND_URL: process.env.NEXT_PUBLIC_BACKEND_URL
}
3. Dockerfile (Build Arguments)
Declares build-time variables:
# Must declare as build arguments
ARG MAILGUN_WEBHOOK_SECRET
ARG NEXT_PUBLIC_BACKEND_URL
# Set as environment variables for build
ENV MAILGUN_WEBHOOK_SECRET=$MAILGUN_WEBHOOK_SECRET
ENV NEXT_PUBLIC_BACKEND_URL=$NEXT_PUBLIC_BACKEND_URL
4. get-firebase-config.sh (Secret Manager Integration)
Converts Secret Manager secrets to Docker build arguments:
# Downloads secrets from GCP Secret Manager
gcloud secrets versions access latest --secret=FIREBASE_ENV > .env.local
# Converts to Docker build arguments
DOCKER_ARGS=$(grep -E "^(NEXT_PUBLIC_|MAILGUN_WEBHOOK_SECRET)" .env.local | sed 's/^/--build-arg /' | tr '\n' ' ')
Production Deployment Process
Step 1: Add Variables to Secret Manager
# Download current secrets
gcloud secrets versions access latest --secret=FIREBASE_ENV --project YOUR_PROJECT_ID > .env.local
# Add new variables
echo "MAILGUN_WEBHOOK_SECRET=your-webhook-secret" >> .env.local
echo "NEXT_PUBLIC_BACKEND_URL=https://your-backend.com" >> .env.local
# Upload back to Secret Manager
gcloud secrets versions add FIREBASE_ENV --data-file=.env.local --project YOUR_PROJECT_ID
Step 2: Update Configuration Files
- Add to
Dockerfile(build arguments and environment variables) - Add to
next.config.mjs(Next.js configuration) - Update
get-firebase-config.sh(if needed for new patterns)
Step 3: Deploy
Cloud Build will automatically:
- Download secrets from Secret Manager
- Convert them to Docker build arguments
- Build the frontend with embedded variables
- Deploy to Cloud Run
Accessing Environment Variables
In API Routes (Server-Side)
// pages/api/example.js or app/api/example/route.js
export async function POST(req) {
const webhookSecret = process.env.MAILGUN_WEBHOOK_SECRET; // ✅ Available
const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL; // ✅ Available
// Use variables...
}
In Client Components (Browser)
// components/MyComponent.tsx
export default function MyComponent() {
const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL; // ✅ Available
const webhookSecret = process.env.MAILGUN_WEBHOOK_SECRET; // ❌ undefined (server-only)
// Use public variables only...
}
Using Next.js Runtime Config
import getConfig from 'next/config';
const { serverRuntimeConfig, publicRuntimeConfig } = getConfig();
// Server-side only
const webhookSecret = serverRuntimeConfig.MAILGUN_WEBHOOK_SECRET;
// Available everywhere
const backendUrl = publicRuntimeConfig.NEXT_PUBLIC_BACKEND_URL;
Common Issues and Solutions
Issue 1: Variable Undefined in Production
Symptom: Variable works locally but is undefined in production
Cause: Variable not added to Docker build process
Solution:
- Add to Secret Manager (
FIREBASE_ENV) - Add to
Dockerfileas ARG and ENV - Update
get-firebase-config.shif needed - Add to
next.config.mjs - Redeploy
Issue 2: Server-Only Variable Exposed to Client
Symptom: Secret appears in browser console or client-side code
Cause: Variable has NEXT_PUBLIC_ prefix or is in publicRuntimeConfig
Solution:
- Remove
NEXT_PUBLIC_prefix - Move to
serverRuntimeConfigonly - Access only in API routes or server components
Issue 3: Variable Not Updated After Deployment
Symptom: Old variable value persists after updating Secret Manager
Cause: Variables are embedded at build time, not runtime
Solution:
- Update Secret Manager
- Trigger new deployment (variables are build-time, not runtime)
- Wait for new build to complete
Issue 4: Build Fails with “Variable Not Found”
Symptom: Docker build fails with missing environment variable
Cause: Variable declared in Dockerfile but not provided by build process
Solution:
- Check variable exists in Secret Manager
- Verify
get-firebase-config.shincludes the variable pattern - Check Cloud Build logs for secret download issues
Debugging Environment Variables
Local Development
// Add to any component or API route
console.log('All env vars:', process.env);
console.log('Specific var:', process.env.MAILGUN_WEBHOOK_SECRET);
Production Debugging
Create a debug API route:
// app/api/debug/env/route.js
export async function GET() {
return Response.json({
mailgunConfigured: !!process.env.MAILGUN_WEBHOOK_SECRET,
backendUrl: process.env.NEXT_PUBLIC_BACKEND_URL,
nodeEnv: process.env.NODE_ENV,
// Don't log actual secret values in production!
});
}
Access: https://your-app.com/api/debug/env
Cloud Build Debugging
Check Cloud Build logs for:
- Secret Manager download success
- Docker build arguments being passed
- Environment variable processing
Security Best Practices
Do:
- ✅ Use server-only variables for secrets
- ✅ Use
NEXT_PUBLIC_only for truly public config - ✅ Validate all environment variables at startup
- ✅ Use TypeScript for environment variable types
Don’t:
- ❌ Put secrets in
NEXT_PUBLIC_variables - ❌ Log secret values in production
- ❌ Commit
.env.localto git - ❌ Use client-side variables for authentication
Environment Variable Validation
Create a validation function:
// lib/env-validation.js
export function validateEnvironmentVariables() {
const required = [
'MAILGUN_WEBHOOK_SECRET',
'NEXT_PUBLIC_BACKEND_URL'
];
const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
}
}
// Call in app startup
validateEnvironmentVariables();
TypeScript Environment Variables
Create type definitions:
// types/env.d.ts
declare namespace NodeJS {
interface ProcessEnv {
MAILGUN_WEBHOOK_SECRET: string;
NEXT_PUBLIC_BACKEND_URL: string;
NEXT_PUBLIC_FIREBASE_API_KEY: string;
// ... other variables
}
}
Quick Reference
| Stage | File | Purpose |
|---|---|---|
| Local | .env.local |
Development environment variables |
| Config | next.config.mjs |
Next.js variable declarations |
| Build | Dockerfile |
Docker build-time arguments |
| Deploy | get-firebase-config.sh |
Secret Manager integration |
| Production | Secret Manager | Secure variable storage |
Adding New Variables Checklist
When adding a new environment variable:
- Add to
.env.local.examplewith placeholder - Add to
Dockerfile(ARG and ENV) - Add to
next.config.mjs(appropriate section) - Update
get-firebase-config.shif needed - Add to Secret Manager (
FIREBASE_ENV) - Update documentation
- Test locally and in production
- Add TypeScript definitions
Troubleshooting Commands
# Check Secret Manager content
gcloud secrets versions access latest --secret=FIREBASE_ENV --project YOUR_PROJECT_ID
# Test local build with Docker
docker build --build-arg MAILGUN_WEBHOOK_SECRET=test-secret .
# Debug Cloud Build
gcloud builds list --limit=1
gcloud builds log [BUILD_ID]
# Check deployed environment
curl https://your-app.com/api/debug/env
This guide ensures that frontend environment variables are properly configured across all deployment stages, from local development to production.