How to Avoid: Secret behind NEXT_PUBLIC_
August 2, 2026 · 6-minute read · Fairy
The short answer
To prevent secrets behind NEXT_PUBLIC_ in AI-generated code, never prefix sensitive API keys with NEXT_PUBLIC_. This prefix exposes variables to client-side JavaScript bundles. Keep secrets like Stripe keys and database credentials server-side only using process.env without the NEXT_PUBLIC_ prefix, and access them exclusively in API routes or server components.
How to Avoid: Secret behind NEXT_PUBLIC_
Any environment variable prefixed with NEXT_PUBLIC_ in Next.js gets bundled into your client-side JavaScript. If that variable contains a secret API key, you've just shipped your credentials to every browser that loads your application. This is a critical security vulnerability that AI code generators produce frequently—and it's one of the easiest mistakes to miss in review.
The fix is straightforward: never prefix sensitive keys with NEXT_PUBLIC_. Keep secrets server-side only, access them through API routes or server components, and reserve NEXT_PUBLIC_ exclusively for values designed for public exposure.
Why This Vulnerability Exists in AI-Generated Code
AI models are trained to produce working code quickly. When you ask for a feature that needs API access from a client component, the model takes the path of least resistance: expose the variable via NEXT_PUBLIC_ so the client can use it directly.
The model doesn't inherently understand the difference between a publishable key (designed for client exposure) and a secret key (which must never leave your server). It sees that NEXT_PUBLIC_ makes the variable available where it's needed, and generates code accordingly.
This pattern appears across several common scenarios:
Payment Integration
// AI-generated code — DANGEROUS
const stripe = new Stripe(process.env.NEXT_PUBLIC_STRIPE_SECRET_KEY);
The model knows Stripe needs initialization, sees that the code runs client-side, and adds NEXT_PUBLIC_ to make it work. Your live Stripe secret key is now in every browser.
Database Connections
// AI-generated code — DANGEROUS
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY
);
The Supabase service role key bypasses all Row Level Security. Exposing it client-side gives any user full database access—read, write, delete, everything.
Third-Party APIs
// AI-generated code — DANGEROUS
const response = await fetch('https://api.openai.com/v1/chat/completions', {
headers: {
'Authorization': `Bearer ${process.env.NEXT_PUBLIC_OPENAI_API_KEY}`
}
});
Your OpenAI API key is now visible in network requests. Anyone can extract it and run up your bill.
How to Detect NEXT_PUBLIC_ Secrets
Manual Code Search
Run these searches against your codebase:
# Find suspicious NEXT_PUBLIC_ variables
grep -r "NEXT_PUBLIC_.*SECRET" .
grep -r "NEXT_PUBLIC_.*KEY" . | grep -v "PUBLIC_KEY"
grep -r "NEXT_PUBLIC_.*SERVICE" .
grep -r "NEXT_PUBLIC_.*ROLE" .
Bundle Inspection
Check what actually ships to browsers:
# After building, search the client bundles
grep -r "sk_live_" .next/static/
grep -r "service_role" .next/static/
grep -r "sk_test_" .next/static/
If any of these return results, secrets are in your client bundle.
Environment File Review
Audit your .env files directly:
# List all NEXT_PUBLIC_ variables
grep "NEXT_PUBLIC_" .env .env.local .env.production
For each result, ask: "Would I be comfortable if this value appeared in the browser's DevTools?" If the answer is no, remove the NEXT_PUBLIC_ prefix.
The Correct Pattern: Server-Side Secret Access
Secrets should only exist in server-side code. Client components request data from your API routes, which access secrets securely.
Before: Secret Exposed Client-Side
// components/PaymentButton.tsx — WRONG
'use client';
import Stripe from 'stripe';
export function PaymentButton() {
const handlePayment = async () => {
// Secret key exposed to browser
const stripe = new Stripe(process.env.NEXT_PUBLIC_STRIPE_SECRET_KEY);
const session = await stripe.checkout.sessions.create({
// ...
});
};
return <button onClick={handlePayment}>Pay</button>;
}
After: Secret Stays Server-Side
// app/api/create-checkout/route.ts — CORRECT
import Stripe from 'stripe';
// Secret accessed only on server — no NEXT_PUBLIC_ prefix
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
export async function POST(request: Request) {
const session = await stripe.checkout.sessions.create({
// ...
});
return Response.json({ sessionId: session.id });
}
// components/PaymentButton.tsx — CORRECT
'use client';
export function PaymentButton() {
const handlePayment = async () => {
// Client calls API route, never sees the secret
const response = await fetch('/api/create-checkout', { method: 'POST' });
const { sessionId } = await response.json();
// Redirect to Stripe with session ID
};
return <button onClick={handlePayment}>Pay</button>;
}
Supabase: Client Key vs Service Role Key
Supabase provides two keys for a reason:
// Client-side code — use anon key only
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL, // URL is fine to expose
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY // Anon key respects RLS
);
// Server-side code — service role stays here
const supabaseAdmin = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_ROLE_KEY // No NEXT_PUBLIC_ — server only
);
The anon key is designed for client exposure—Row Level Security controls what users can access. The service role key bypasses all security and must never reach the client.
Keys That Are Safe for NEXT_PUBLIC_
Some keys are explicitly designed for public exposure:
- Stripe publishable key (
pk_live_*,pk_test_*) — used for Stripe Elements - Firebase client config — project ID, API key for client SDK
- Analytics IDs — Google Analytics, Segment write keys
- Supabase anon key — designed for client use with RLS
- Public API endpoints — URLs without authentication requirements
The pattern: if the provider's documentation tells you to use this key client-side, it's safe for NEXT_PUBLIC_. If the documentation says "keep this secret" or "server-side only," it's not.
Responding to Exposed Secrets
If you've already deployed code with a NEXT_PUBLIC_ secret:
-
Rotate the key immediately. The exposed key is compromised. Generate a new one in your provider's dashboard.
-
Remove from client code. Delete the
NEXT_PUBLIC_prefix and move all usage to server-side code. -
Check for abuse. Review logs in Stripe, your database, or other affected services for unauthorized access.
-
Redeploy. Even after removing the variable, old bundles may be cached. Force a fresh deployment.
-
Audit version control. If the secret was committed to git, it exists in history. Consider it permanently compromised even after removal.
Automated Prevention
ESLint Rules
Create a custom rule to flag suspicious patterns:
// .eslintrc.js
module.exports = {
rules: {
'no-restricted-syntax': [
'error',
{
selector: 'Literal[value=/NEXT_PUBLIC_.*SECRET/i]',
message: 'Secrets must not use NEXT_PUBLIC_ prefix'
},
{
selector: 'Literal[value=/NEXT_PUBLIC_.*SERVICE_ROLE/i]',
message: 'Service role keys must not use NEXT_PUBLIC_ prefix'
}
]
}
};
Pre-Commit Hooks
Block commits containing suspicious patterns:
#!/bin/sh
# .git/hooks/pre-commit
if git diff --cached | grep -E "NEXT_PUBLIC_.*(SECRET|SERVICE_ROLE|SK_LIVE)"; then
echo "ERROR: Potential secret in NEXT_PUBLIC_ variable"
exit 1
fi
CI/CD Checks
Add a build step that fails if secrets appear in client bundles:
# In your CI config
- name: Check for exposed secrets
run: |
npm run build
if grep -r "sk_live_\|service_role" .next/static/; then
echo "CRITICAL: Secrets found in client bundle"
exit 1
fi
Why AI Review Matters for This Class of Bug
The NEXT_PUBLIC_ prefix is syntactically correct. The code will build, run, and appear to work perfectly. Standard linters don't flag it because there's no syntax error—just a catastrophic security misconfiguration.
This is exactly the type of issue that requires domain-aware review. You need verification that understands Next.js's environment variable semantics, recognizes which keys are secrets based on naming patterns and provider conventions, and flags exposure regardless of whether the code executes correctly.
Fairy Scout performs this analysis automatically on AI-generated pull requests, catching NEXT_PUBLIC_ secrets before they reach production. For systematic AI code verification across your organization, Fairy for Code provides continuous oversight with expert review for the judgment calls automated tools can't make.
Summary
The rule is simple: if a key is secret, it cannot have the NEXT_PUBLIC_ prefix. AI code generators don't reliably enforce this boundary, so you need to:
- Audit every
NEXT_PUBLIC_variable—ask if you'd show it publicly - Keep secrets in server-side code only
- Have client components call API routes instead of using secrets directly
- Automate detection with linting, hooks, and CI checks
- Review AI-generated code with tools that understand these patterns
Secrets exposed via NEXT_PUBLIC_ is one of the most common critical vulnerabilities in AI-generated Next.js code. It's also one of the most preventable—once you know what to look for.
Frequently asked questions
What happens if I use NEXT_PUBLIC_ with a secret key?
The secret gets bundled into your client-side JavaScript and shipped to every browser. Anyone can open DevTools, search the bundle, and extract your API key. This exposes billing accounts, databases, and third-party services.
Why does AI generate NEXT_PUBLIC_ secrets?
AI models optimize for code that works immediately. When client components need data, the fastest path is NEXT_PUBLIC_ variables. The model doesn't distinguish between publishable keys meant for clients and secret keys that must stay server-side.
How do I access secrets in Next.js without exposing them?
Use API routes or server components. Call process.env.YOUR_SECRET_KEY from server-side code only. Client components fetch data from your API routes, which access secrets securely on the server.
Which keys are safe to use with NEXT_PUBLIC_?
Only publishable or public keys designed for client exposure. Examples include Stripe's publishable key (pk_live_*) or analytics tracking IDs. Never use NEXT_PUBLIC_ with secret keys, service role keys, or anything prefixed sk_ or containing 'secret'.
How do I detect NEXT_PUBLIC_ secrets in my codebase?
Search for patterns like NEXT_PUBLIC_.*SECRET, NEXT_PUBLIC_.*KEY where the variable name suggests a secret. Also grep your built bundles in .next/static for known key prefixes like sk_live_ or service_role.
Have AI-generated work you’d want verified? Connect with a Fairy → or run a free check with Scout.
More resources