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

  1. Add to Dockerfile (build arguments and environment variables)
  2. Add to next.config.mjs (Next.js configuration)
  3. Update get-firebase-config.sh (if needed for new patterns)

Step 3: Deploy

Cloud Build will automatically:

  1. Download secrets from Secret Manager
  2. Convert them to Docker build arguments
  3. Build the frontend with embedded variables
  4. 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:

  1. Add to Secret Manager (FIREBASE_ENV)
  2. Add to Dockerfile as ARG and ENV
  3. Update get-firebase-config.sh if needed
  4. Add to next.config.mjs
  5. 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:

  1. Remove NEXT_PUBLIC_ prefix
  2. Move to serverRuntimeConfig only
  3. 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:

  1. Update Secret Manager
  2. Trigger new deployment (variables are build-time, not runtime)
  3. 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:

  1. Check variable exists in Secret Manager
  2. Verify get-firebase-config.sh includes the variable pattern
  3. 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:

  1. Secret Manager download success
  2. Docker build arguments being passed
  3. 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.local to 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.example with placeholder
  • Add to Dockerfile (ARG and ENV)
  • Add to next.config.mjs (appropriate section)
  • Update get-firebase-config.sh if 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.