How an API key escapes an AI-built Next.js app
A privileged key read inside a client component ends up in the public bundle. Here is how the leak happens and the three checks that catch it before launch.
· Zoltra Research
An AI coding tool will happily read a privileged credential inside a client component. Nothing in the framework stops it. The key ends up in the JavaScript bundle, the bundle is public, and the database it unlocks answers to anyone who opens developer tools.
This is the most common serious finding we see on AI-built sites, and it is also the easiest one to check for. Here is how the leak actually happens and the three checks that catch it before launch.
The two ways a server value becomes a browser value
It is read in a client component. In Next.js, a component marked "use client"
runs in the browser. Any variable it reads is shipped to the browser. If that
variable holds a service-role key, the key is now public. Next.js only inlines
variables that begin with NEXT_PUBLIC_, but a non-prefixed value that is read at
runtime inside a client component still travels — through props, through a
context provider, or through an inline JSON.stringify of a config object. The
prefix rule protects the framework's own substitution, not your data flow.
It is read in a route handler with no server boundary. A route handler is
server code until a client component imports from it. An AI tool that generates a
shared lib/supabase.ts and imports it from both a page and a route handler has
created a module that is now reachable from the browser. The credential inside it
goes along.
Neither mistake looks like a mistake in a diff. Both look like normal code.
Why the blast radius is bigger than one key
A leaked service-role key is not a read-only key. It bypasses row level security by design. That turns one mistake into three:
- Data. Every row in every table the key can reach, not just the rows the application would have shown.
- Write access. Data can be altered or deleted, not only read.
- Cost. Some managed backends bill per request. An unauthenticated endpoint with a valid privileged key is an open invitation to a bill nobody budgeted for.
The same shape appears in other services. An OpenAI-style key, a mail provider key, or a payment secret in a client bundle all produce a version of the same outcome: somebody else can spend your money or act as you.
The three checks that catch it before launch
1. Search the built output, not the source
Source review misses this class entirely, because the leak is created at build time. Build the app the way production builds it, then search the generated JavaScript for a credential pattern:
npm run build
grep -rEn "service_role|sk_live|sk-[A-Za-z0-9]{20,}" .next/static/ | head
If that command prints a real key, the site is already leaking. Rotate the credential first — deleting it from the code does not un-publish it.
2. Make the server boundary explicit
Every module that touches a privileged client should declare that it is server only, so an accidental browser import fails at build time instead of shipping:
import "server-only";
Next.js then refuses to bundle that module into a client component. This is the handful of characters that turns a silent leak into a build error. Route handlers that must stay server-side get the same treatment.
3. Assume a public key is public, then check what it can do
The question is not only "did the key leak" but "what could somebody do with it". For a managed backend, that means row level security is on and the default is deny. A policy that is enabled but defaults to allow is the same as no policy. Confirm the privileged credential is never the one the browser holds: the browser should hold an anon key scoped by policy, and the privileged key should exist only in the server environment.
What to do when you find one
- Rotate first. Treat the credential as compromised the moment you confirm it is in a public bundle. Rotation is the only thing that revokes it.
- Then remove the path that put it there — move the call behind a server boundary, or move the value out of the browser's reach.
- Then check the blast radius — what the key could reach, and whether anything was read or written that should not have been.
- Then re-check the built output, and confirm the check is part of how you ship from now on.
What we are not saying
Not every AI-built app leaks a key. The tools are not uniquely careless; they are fast, and speed is exactly what skips this check. A handmade app written the same way leaks the same key.
We are also not saying a scanner finds everything. This check is narrow on purpose: it looks at what a public site exposes and reports what it can prove. A clean result means the checks that ran passed, not that the application is secure.
If you want the narrow version of this check run against your own site right now, Zoltra runs a free first look that reports what it can see on your public surface.
Related
Sources
- Next.js: environment variables (accessed 2026-09-21)
- Next.js: route handlers (accessed 2026-09-21)
- OWASP Top 10 A05:2021 Security Misconfiguration (accessed 2026-09-21)
- OWASP Top 10 A01:2021 Broken Access Control (accessed 2026-09-21)
This is a measurement and explanation, not a guarantee. Zoltra reports what it can observe on a public site and states what it cannot see.