Secret API Key Exposed in Client Bundle
A sensitive credential (Stripe secret, OpenAI API key, Supabase service role key) is prefixed with NEXT_PUBLIC_, causing it to be inlined into the browser JavaScript bundle where anyone can read it.
Typical error
NEXT_PUBLIC_ prefix used for private key
What this is
In Next.js, environment variables prefixed with NEXT_PUBLIC_ are inlined into the client bundle at build time. Any visitor can open devtools, open Sources, and read them.
Legitimate uses: analytics keys intended to be public, feature flags, public API URLs.
Catastrophic misuses FinishKit catches:
NEXT_PUBLIC_STRIPE_SECRET_KEYNEXT_PUBLIC_OPENAI_API_KEYNEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEYNEXT_PUBLIC_SENDGRID_API_KEY
Any of the above lets an attacker bill your account, send emails from you, or read all your database rows bypassing RLS.
Why AI tools ship this
When the generated code needs a credential, the prompt-to-app builder often reaches for the most accessible env var pattern. NEXT_PUBLIC_ appears in examples everywhere, so the tool defaults to it. The generated app runs, the secret works, nobody checks the bundle output.
How to detect
Search the built output for known secret patterns:
grep -r "sk_live_" .next/static
grep -r "sk_test_" .next/static
grep -r "service_role" .next/static
grep -rE "AKIA[0-9A-Z]{16}" .next/staticAny hit means the secret is shipped to every visitor.
How to fix
- Rotate the secret immediately at the provider (Stripe, OpenAI, Supabase dashboard).
- Move the value to a non-public env var: rename
NEXT_PUBLIC_STRIPE_SECRET_KEYtoSTRIPE_SECRET_KEY. - Move any code that reads the secret into a server component, a route handler, or a server action. Never read it from a
'use client'component. - Add a pre-commit scanner like gitleaks or trufflehog to prevent a recurrence.
Related
- Glossary: secret exposure
- Glossary: environment variable