Supabase Env Vars Missing in Production

Supabase URL or anon key undefined in prod — host env config or prefix mismatch.

Your local .env has NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY. createClient(url, anonKey) works fine. Deploy to Vercel / Netlify / Cloudflare Pages and the frontend console screams:

TypeError: Cannot read properties of undefined (reading 'auth')
Error: supabaseUrl is required.

Or worse — silent failure: every Supabase call returns nothing, page renders but is empty. The platform doesn’t have the env, the prefix is wrong, or build cache is stale → process.env.X in the client bundle is undefined.

Mental model: env vars don’t auto-sync from .env to the cloud. Each platform requires manual config; client-visible env requires a specific prefix (NEXT_PUBLIC_ / VITE_ / PUBLIC_).

Common causes

Ordered by hit rate, highest first.

1. Platform doesn’t have these env vars at all

Most common. You have a local .env, but Vercel dashboard has nothing. .env isn’t in git (and shouldn’t be), so after deploy process.env.X === undefined.

How to spot it: Vercel / Netlify / CF Pages dashboard → Environment Variables → are they listed?

2. Prefix mismatch with framework

Next.js → NEXT_PUBLIC_X     (client-visible)
Vite    → VITE_X            (import.meta.env.VITE_X)
Astro   → PUBLIC_X          (import.meta.env.PUBLIC_X)
SvelteKit → PUBLIC_X        ($env/static/public)
Remix   → no enforced prefix, PUBLIC_ recommended

Missing the prefix means the env isn’t bundled — undefined when accessed.

How to spot it: Are your env names properly prefixed?

3. Build cache holds the old env

You just added the env but Vercel deployed from a cached build → bundle still missing the new var.

How to spot it: Just added env, redeployed, problem persists.

4. Only set in Preview, not Production

Vercel env scopes are Development / Preview / Production. Tick only Preview → Production deploy is still undefined.

How to spot it: Vercel dashboard → check env var’s environment scope.

5. Trying to use server-only env on the client

SUPABASE_SERVICE_ROLE_KEY (no NEXT_PUBLIC_) is server-only — undefined on client. And that’s good: service role key has full DB power, should never go to the client.

How to spot it: You’re reading a server-only env in a client component.

6. Trailing whitespace / newline in env value

Copy-pasted anon key picked up a space or newline; URL parse fails on init.

How to spot it: Manual trim fixes it.

Shortest path to fix

Step 1: Copy correct values from Supabase

Supabase Dashboard → Project → Settings → API
  → Project URL (https://xxx.supabase.co)
  → anon public key (eyJ... long string)

Don’t pick up trailing whitespace.

Step 2: Add env on the deploy platform

Vercel:
  Settings → Environment Variables
  → Add NEXT_PUBLIC_SUPABASE_URL = https://xxx.supabase.co
  → Add NEXT_PUBLIC_SUPABASE_ANON_KEY = eyJ...
  → Environment: tick Production + Preview + Development (all of them)

Netlify:
  Site settings → Environment variables → Add a variable

Cloudflare Pages:
  Settings → Environment variables → Production

Confirm all environments (Production / Preview / Development) are ticked.

Step 3: Grep code for the right prefix

# Next.js
grep -r "process.env.SUPABASE" src/
# All should be process.env.NEXT_PUBLIC_SUPABASE_*

# Vite
grep -r "import.meta.env" src/
# All should be import.meta.env.VITE_SUPABASE_*

# Astro
grep -r "import.meta.env" src/
# All should be import.meta.env.PUBLIC_SUPABASE_*

Fix any that don’t match:

// Next.js
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);

Step 4: Force a no-cache redeploy

Vercel:
  Deployments → latest → ... → Redeploy
  → uncheck "Use existing Build Cache"

Netlify:
  Deploys → Trigger deploy → "Clear cache and deploy site"

Cloudflare Pages:
  Auto-uses latest commit; force = retry deployment

Step 5: Verify on the live site

Open prod URL, DevTools Console:

// Next.js
console.log(process.env.NEXT_PUBLIC_SUPABASE_URL);
// Should print https://xxx.supabase.co, not undefined

// Vite
console.log(import.meta.env.VITE_SUPABASE_URL);

Still undefined → wrong prefix or build cache wasn’t cleared.

Step 6: Boot-time validation

Add fail-fast validation so the next missing var breaks the build, not runtime:

// lib/env.ts
import { z } from 'zod';

const envSchema = z.object({
  NEXT_PUBLIC_SUPABASE_URL: z.string().url(),
  NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string().min(20),
});

export const env = envSchema.parse({
  NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL,
  NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
});

Build fails on missing env — orders of magnitude better than runtime discovery.

Step 7: Keep server-only env distinct

// ✅ Client-visible
NEXT_PUBLIC_SUPABASE_URL
NEXT_PUBLIC_SUPABASE_ANON_KEY

// ❌ Server only — never add the NEXT_PUBLIC_ prefix
SUPABASE_SERVICE_ROLE_KEY  // full DB privilege; leaking = disaster

Prevention

  • List required env in README / CLAUDE.md, including the prefix
  • Keep .env.example (no real values) in sync with .env; new contributors copy from it
  • Validate env with zod at boot; fail-fast on missing
  • Any env with _KEY, _SECRET, _TOKEN must never carry NEXT_PUBLIC_ / VITE_ / PUBLIC_ prefix
  • When you add a new env, sync local + Preview + Production immediately
  • After env changes, default to “Clear cache and redeploy” — don’t assume incremental builds pick up new values
  • Add env changes to the PR template / deploy checklist
  • Monitor boot logs and alert on env validation failures

Tags: #Backend #Debug #Troubleshooting #Supabase