NEXT_PUBLIC secrets: how API keys get exposed in Next.js
Any environment variable prefixed NEXT_PUBLIC_ is bundled into your JavaScript and served to every visitor — no authentication, no server call. Here's how to check if you're affected and how to fix it.
How NEXT_PUBLIC_ works
During a Next.js build, all references to process.env.NEXT_PUBLIC_* are statically replaced with their literal string values. That value is embedded directly in the JavaScript bundle that ships to every browser. There is no runtime lookup — the string is baked in at build time.
This is intentional for values that are meant to be public. The problem is that AI coding tools — and developers under time pressure — often use this prefix for convenience on secrets that should never leave the server.
.env.local (what you write):
NEXT_PUBLIC_OPENAI_API_KEY=sk-proj-abc123...
// In your component (what you write):
const key = process.env.NEXT_PUBLIC_OPENAI_API_KEY
// In the browser bundle (what gets shipped):
const key = "sk-proj-abc123...".env file does not remove it from deployed bundles or from git history. You must rotate the credential at the provider.What is and isn't safe to expose
Not all public variables are dangerous. Some keys are designed to be public. The key question is: what can someone do with this value if they have it?
SAFE TO EXPOSE
- Stripe publishable key (
pk_live_...) - Supabase anon key with RLS enabled
- PostHog project token
- Google Analytics / GTM IDs
- Public app URLs and feature flags
NEVER EXPOSE
- OpenAI / Anthropic API key
- Stripe secret key (
sk_live_...) - Supabase
service_rolekey - Database connection strings
- JWT signing secrets / encryption keys
- Webhook secrets
How to check if you're affected
1. Grep your source files
Run this from your project root to list every NEXT_PUBLIC_ variable reference:
grep -rn "NEXT_PUBLIC_" . \
--include="*.ts" --include="*.tsx" \
--include="*.js" --include="*.jsx" \
--include=".env*" \
--exclude-dir=node_modulesFlag any variable whose value looks like an API key or secret — long random strings, keys starting with known prefixes (sk-, op_, ghp_, etc.).
2. Search the live JS bundle
Open your deployed site. Open DevTools → Sources (Chrome) or Debugger (Firefox). Press Ctrl+Shift+F to search all files. Paste the first 8 characters of your secret.
# Or search from the terminal against your live bundle:
curl -s https://yourapp.com/_next/static/chunks/app/layout.js \
| grep -o "sk-proj-[a-zA-Z0-9]*"3. Check your .env files for dangerous NEXT_PUBLIC_ values
# List .env values that are NEXT_PUBLIC_ and look like secrets
grep -h "^NEXT_PUBLIC_" .env .env.local .env.production 2>/dev/null \
| grep -v "=$" \
| grep -E "(KEY|SECRET|TOKEN|PASSWORD|PASS|PRIVATE|SIGNING)" How to fix it
Step 1 — Rotate the exposed credential immediately
Go to the service dashboard and revoke or rotate the key before writing any code. The old value is in your bundle and potentially in git history — no code change makes the old key safe.
Step 2 — Remove the NEXT_PUBLIC_ prefix
# Before (❌ exposed to browser)
NEXT_PUBLIC_OPENAI_API_KEY=sk-proj-...
# After (✅ server-only)
OPENAI_API_KEY=sk-proj-...Step 3 — Move the usage to server-only code
Server-only code in Next.js: Server Components (no 'use client' directive), Route Handlers (app/api/**/route.ts), and Server Actions ('use server').
// ❌ Before — client component, key is in the bundle
'use client'
export function AiChat() {
const response = await fetch('https://api.openai.com/v1/chat/completions', {
headers: { Authorization: `Bearer ${process.env.NEXT_PUBLIC_OPENAI_API_KEY}` }
})
}
// ✅ After — server action, key never leaves the server
'use server'
export async function askAi(prompt: string) {
const response = await fetch('https://api.openai.com/v1/chat/completions', {
headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}` }
})
return response.json()
}server-only at the top of the file. Next.js will throw a build error if that module is imported from a client component.import 'server-only'
export function callOpenAI(prompt: string) { ... }Step 4 — Clean git history (if the key was committed)
Rotating the key is more important than cleaning git history (a rotated key is worthless), but you should still remove it to avoid confusing future developers:
# Use git-filter-repo (preferred over BFG):
pip install git-filter-repo
git filter-repo --path .env --invert-paths
# Or remove a specific string across all history:
git filter-repo --replace-text <(echo "sk-proj-OLD_KEY==>REDACTED")How vibecheck detects this
When you connect your GitHub repo, vibecheck runs two layers of detection:
- Gitleaks scans git history and current files for secrets matching 150+ provider-specific patterns (OpenAI, Stripe, Supabase, AWS, GitHub, etc.).
- Custom NEXT_PUBLIC_ check specifically looks for environment variable declarations where the key name contains
NEXT_PUBLIC_and the value matches a secret pattern — flagging cases Gitleaks might miss because the value format is ambiguous.
Scan your repo for this and 42 other checks
Connect your GitHub repo and vibecheck runs Gitleaks, Semgrep, njsscan, and 43 custom checks — then Claude explains every finding in plain English. Free for 2 repos.
scan my repo for free →FAQ
Is it fine if I need the key in the browser for a direct API call?
Almost never. Almost all AI/payment/database APIs have server-side SDKs and you should proxy the call through a Route Handler. Direct browser-to-API calls also expose your key in network requests visible to anyone with DevTools.
My Supabase anon key is NEXT_PUBLIC_ — is that ok?
Yes, if Row Level Security is enabled on all your tables. The anon key is designed to be public — it only grants the permissions your RLS policies allow. Without RLS enabled, the anon key becomes a master key. Check your RLS status first.
Does Vercel / Railway protect NEXT_PUBLIC_ variables?
No. The platform keeps the variable value secret during the build, but it inlines the value into the output bundle. The bundle is then served publicly. The platform cannot protect what Next.js has already embedded in JavaScript.
Can I use NEXT_PUBLIC_ variables in middleware?
Middleware runs on the server (Edge runtime), so technically yes — but if you also reference the variable in a client component, Next.js will bundle it. Use a non-prefixed variable in middleware to be safe.
I removed the variable from .env — am I safe now?
No. The old deployed bundles still contain the value, and anyone who visited the site (or any bot) may have already copied it. Rotate the credential at the provider. The .env change only prevents future builds from including it.
RELATED GUIDES