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,_TOKENmust 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