Environment Variable Missing in Production

Works locally, fails prod — env var not set, or prefix issue (PUBLIC_ / VITE_ / NEXT_PUBLIC_).

Local npm run dev is green, but deploying to Vercel / Netlify / Cloudflare Pages produces a 500, or the frontend throws Cannot read properties of undefined / API key is required. 99% of the time, the cause is an environment variable that exists in your local .env but isn’t reaching the production runtime. Frameworks like Astro, Next.js, and Vite enforce strict prefix rules about which env vars get inlined into the client bundle, and .env files are (correctly) gitignored, so the host has no way to learn about them unless you set them in the dashboard. This article walks through the 5 hit-rate-ranked failure modes and a fix path you can complete in ~10 minutes.

Common causes

Ordered by hit rate, highest first.

1. The variable is never set on the host

You have a .env.local locally, but Vercel / Netlify / Cloudflare Pages’s Environment Variables panel is empty for Production (or only set for Preview / Development). Since .env files aren’t committed (and shouldn’t be), the host literally doesn’t know about them.

# Vercel CLI — list what's actually on production
vercel env ls production
# Expect to see your variable names; if missing, this is the cause

How to spot it: Runtime error process.env.X is undefined, or the build log’s env list doesn’t include the variable name.

2. Client-side code reads a var without the required prefix

Each framework only exposes prefixed vars to the browser bundle. Anything else is silently replaced with undefined at build time:

FrameworkClient-exposed prefix
Next.jsNEXT_PUBLIC_
Vite / Astro / SvelteKitPUBLIC_ (Astro) / VITE_ (Vite)
Create React AppREACT_APP_
Remixmust explicitly return from loader
// Inside an Astro component
const wrong = import.meta.env.API_URL;        // undefined (no prefix)
const right = import.meta.env.PUBLIC_API_URL; // works

How to spot it: DevTools → Console on the live page, evaluate import.meta.env or window.__NEXT_DATA__. If the variable isn’t there, the framework filtered it out.

3. Build cache reused the previous output

You added a new var and redeployed, but Vercel’s build cache reused the prior dist/ and your new var never participated in compilation. Common when you “only change env, don’t change code.”

How to spot it: The build log says Restored build cache and the deploy completes in seconds instead of minutes.

4. Variable name typo or whitespace

.env has STRIPE_SECRET_KEY but the code reads STRIPE_SECRET; or the Vercel dashboard has a trailing space in the name. Env var names are case-sensitive, and dashboard UIs are happy to hide this.

How to spot it:

# Temporary debug print in a serverless function (delete after — don't ship)
console.log(Object.keys(process.env).filter(k => k.includes("STRIPE")))

5. Monorepo (Turborepo / Nx) doesn’t pass env through

Root .env has the var, but a child workspace’s next build doesn’t see it — Turborepo doesn’t forward env to tasks by default, you have to declare them in turbo.json.

// turbo.json
{
  "pipeline": {
    "build": {
      "env": ["DATABASE_URL", "NEXT_PUBLIC_API_URL"]
    }
  }
}

How to spot it: cd apps/web && npm run build works locally, but turbo build from the root fails — the var isn’t crossing the task boundary.

Shortest path to fix

Work in order: confirm the var is on the host, confirm the prefix matches the framework, force a cacheless rebuild.

Step 1: Add the variable to the Production environment

By platform:

  • Vercel: Project → Settings → Environment Variables → Add New → name / value / check Production (uncheck Preview / Development unless needed).
  • Netlify: Site → Site configuration → Environment variables → Add a variable.
  • Cloudflare Pages: Settings → Environment variables → Production → Add variable.
  • Firebase Hosting + Functions: firebase functions:config:set or Secret Manager.
# Vercel CLI works too
vercel env add STRIPE_SECRET_KEY production
# It will prompt for the value interactively

For secrets (API keys, DB passwords) use the platform’s “Sensitive” / “Secret” toggle, otherwise the value appears in build logs in plaintext.

Step 2: Add the framework’s client prefix where required

Use the right prefix in both the var name and the code:

# .env.production
NEXT_PUBLIC_API_URL=https://api.example.com    # Next.js client
PUBLIC_API_URL=https://api.example.com         # Astro client
VITE_API_URL=https://api.example.com           # Vite client
DATABASE_URL=postgres://...                    # server only, no prefix

In code:

// Next.js
const url = process.env.NEXT_PUBLIC_API_URL;

// Astro / Vite
const url = import.meta.env.PUBLIC_API_URL;

The host dashboard variable name must match the prefix exactly — no abbreviating.

Step 3: Redeploy with build cache disabled

# Vercel CLI
vercel --prod --force

Or in the dashboard: Deployments → most recent → ⋯ → Redeploy, uncheck “Use existing Build Cache”.

Netlify: Deploys → Trigger deploy → “Clear cache and deploy site”.

Cloudflare Pages: Deployments → Retry deployment (Pages always rebuilds without cache).

Step 4: Add a startup schema check to fail fast

Validate required env at startup with Zod so a missing var crashes the build / boot rather than producing a runtime 500:

// src/env.ts
import { z } from "zod";

const schema = z.object({
  DATABASE_URL: z.string().url(),
  STRIPE_SECRET_KEY: z.string().startsWith("sk_"),
  NEXT_PUBLIC_API_URL: z.string().url(),
});

export const env = schema.parse(process.env);

A missing var now throws ZodError during build/boot, which is much easier to debug than a runtime crash on a user request.

Step 5: Verify the live site is actually reading the value

After deploy:

# Server-side var: hit an API route that consumes it
curl -s https://yourdomain.com/api/healthz
# Expect 200 with env-derived data in the response

# Client-side var: grep the HTML / bundle to confirm inlining
curl -s https://yourdomain.com/ | grep -o "https://api\.example\.com" | head -1

Prevention

  • Maintain a .env.example in the repo listing every required variable name (no values); README documents which are required vs optional.
  • Validate env at startup with Zod / Valibot / Envalid so a missing var fails the build instead of a user request.
  • Add a CI check-env step that diffs .env.example against what’s set on the host and fails if anything is missing.
  • Separate .env.public (all client-prefixed) from .env.secrets (all server-only) so accidental client-side leakage is visible in review.
  • After any env change, default to a force-rebuild (--force / “Clear cache”) rather than relying on whoever clicks Deploy to remember.

Tags: #Hosting #Debug #Troubleshooting