"I put the API key in an environment variable, so it's safe." This is where that belief is most often betrayed in AI-built apps.
Putting it in an environment variable is correct in itself. But environment variables come in two kinds — ones that reach the browser and ones that don't — and the boundary is decided purely by how you name them. AI, trying to make things run, sometimes crosses that boundary.
This article explains the mechanism for non-engineers.
1. What NEXT_PUBLIC_ is doing
Next.js has one very clear rule:
When an environment variable's name starts with
NEXT_PUBLIC_, its value is embedded into browser-facing files at build time
This is documented behaviour, not a bug. It is the sanctioned mechanism for values that genuinely need to run in the browser — a public API URL, an analytics measurement ID.
The important word is "embedded." It is not being loaded safely from somewhere at runtime; it is written into the file as a literal string.
[No NEXT_PUBLIC_ prefix]
read only inside the server → never reaches the browser at all
[With NEXT_PUBLIC_ prefix]
embedded into the JavaScript as a string at build time
→ readable by anyone who opens the page
How to see it yourself
You can confirm this on your own app.
- Open the app in a browser
- Press F12 (⌘+Option+I on Mac) for the developer tools
- Open a JavaScript file under the "Sources" tab and search for part of your key
Or more simply, right-click the page → "View page source" and search there. If it's embedded, it comes straight back as a plain string.
2. Why AI crosses the boundary
It isn't malice. It's mechanics.
Suppose you're building and you hit an error: "the data won't load." You ask the AI to fix it. The AI looks for the shortest route to a working state.
At that moment, in a place where browser-side code wants to use the key, adding NEXT_PUBLIC_ definitely works. Without it, the value is undefined and nothing runs.
AI targets "does it run," so it picks the one that runs. And it runs. The error disappears. You conclude it's fixed.
This is the textbook case of running and being safe pointing in opposite directions.
In GitGuardian's 2026 study, commits written with AI assistance leaked secrets at roughly twice the rate (3.2% vs 1.5%) of human-only commits. Individual models aren't being careless — it is the natural consequence of optimising for "make it run fast."
3. Two kinds of key — anon and service_role
If you use Supabase there are two kinds of key. Understand this difference and 80% of the judgement is done.
anon key | service_role key | |
|---|---|---|
| Intent | Public by design | Absolutely secret |
| RLS | Subject to it | Bypasses all of it |
| In the browser | ✓ normal | ✗ catastrophic |
| Use | Ordinary app access | Server-side administrative work |
Why the anon key being visible is fine
The anon key is only "permission to talk to the database." What you can see is decided by RLS. With RLS configured correctly, holding this key still returns zero rows belonging to anyone else.
So the anon key being readable in the browser is normal. There's nothing to worry about.
Why the service_role key is catastrophic
The service_role key is an administrative key that bypasses every restriction, RLS included.
If it leaks, configuring RLS perfectly buys you nothing. Everything is bypassed. Reading, rewriting and deleting every table become possible for anyone.
Why AI wants to use service_role
The reason is plain: no error appears.
- Using the
anonkey → nothing comes back until the RLS policies are written correctly, so you get an error - Using the
service_rolekey → everything works from the start without writing a single policy
If you ask an AI to "write the code that fetches this data" while RLS isn't set up, the anon key won't work — so the AI may pick the one that does.
And that choice looks completely normal on screen.
4. Keys on GitHub — deleting doesn't delete
The other major leak path.
You write keys into a file called .env and upload it to GitHub. It happens.
The problem is that GitHub retains commit history whether the repository is public or private. Even after you delete the file, opening an earlier commit shows the key exactly as it was.
commit 1: add .env (contains the key) ← stays here forever
commit 2: delete .env ← gone from the current state only
"I deleted it, so it's fine" does not hold.
There is only one fix
Reissue the key itself. Revoke the old one in the provider's dashboard, issue a new one, and update your environment variables.
Only once it is revoked does the item count as handled. Creating a new key without revoking the old one leaves the old one alive.
Also check .gitignore
.gitignore is the list of "files never to upload to GitHub." Check that .env* is in it, and add it if not.
5. Easily missed leak paths
Beyond NEXT_PUBLIC_ and GitHub, these happen in practice.
Code pasted into an AI chat. Have you ever pasted a file with the keys still in it and said "fix this error"? Many services have a setting that keeps it out of training, but the fact remains that at that moment it left your control.
Screenshots taken for a question. Part of a key can appear in a screenshot of a terminal or a dashboard. If you posted it to social media or a technical Q&A site, it is published.
Error message logs. AI sometimes writes code that logs the entire request for debugging. If an auth token is in that log, the log storage itself becomes part of your exposure.
Frontend source maps. If you publish source maps in a production build, minified code can be restored to its original form — and embedded values are naturally readable.
The principle is simple. A key is invalid the moment it might have been seen. When in doubt, reissue. Reissuing costs minutes; a leak costs an unbounded amount.
6. How to check for yourself
Step 1: Inventory NEXT_PUBLIC_
Search your project for NEXT_PUBLIC_ and look at each value that comes back. The test is only this:
Would I be fine if this value were in a stranger's hands?
- Public API URL → fine
- Analytics measurement ID → fine
- Supabase
anonkey → fine (assuming RLS) - Supabase
service_rolekey → not fine - Database password → not fine
- OpenAI / Anthropic API keys → not fine
- Stripe secret key (starts with
sk_) → not fine
With Stripe, publishable keys starting with pk_ may go in the browser and secret keys starting with sk_ must never. You can tell them apart by name.
Step 2: Actually look in the browser
Open the production app, view the page source, and search for the first few characters of a key. If it comes back, that key is readable worldwide.
Step 3: Check GitHub
Look for .env in the repository file list. If it's there, check that file's "History" too.
7. The instruction to paste into your AI tool
Search this entire project and check the following three things.
(1) Whether any secrets are stored in environment variables prefixed
NEXT_PUBLIC_ (Supabase service_role key, database passwords,
OpenAI/Anthropic API keys, Stripe secret keys starting with sk_)
(2) Whether any keys or passwords are written directly in the code
(3) Whether .gitignore contains .env* — add it if not
For anything you find, move the value to a server-only environment variable
(no NEXT_PUBLIC_ prefix) and move the code that reads it to the server side
(Route Handler / Server Action). List every place a client component
references a secret.
Important: never print the secret values themselves. Report only which file
and line holds which kind of key.
Finally, for each key you found, tell me which provider dashboard to reissue it
in and which environment variables must be updated afterwards.
That last sentence matters. Moving it is not the fix. A key that was ever on the public side stays valid until it is reissued.
8. What the correct layout looks like
To summarise:
[Fine to reach the browser] add NEXT_PUBLIC_
- public API URLs
- Supabase URL and anon key (assuming RLS is configured correctly)
- analytics measurement IDs
- Stripe publishable keys starting with pk_
[Server only] do not add NEXT_PUBLIC_
- Supabase service_role key
- database connection strings and passwords
- OpenAI / Anthropic API keys
- Stripe secret keys starting with sk_, and the webhook signing secret
- email provider API keys
And production values go only into your host's environment variable settings (Vercel and the like). The .env file is for local development and never goes to GitHub.
Summary
- A value prefixed
NEXT_PUBLIC_is embedded into browser-facing files at build time. That is the specification, not a bug - Supabase's
anonkey is public by design and safe. Theservice_rolekey bypasses everything and must never be in the browser - AI reaches for
service_rolebecause "no error appears without writing RLS." The easy path is the most dangerous one - GitHub commit history does not disappear. Deleting the file is not a fix — the key must be reissued
- Pasting into AI chats, screenshots and logs are leak paths too. "It might have been seen" is already enough to reissue
Key placement is judgeable once you understand the mechanism. The test is one question: would I be fine with a stranger holding this? When in doubt, put it on the server.
To check this alongside RLS, see What is Supabase "RLS"?; for the whole picture, Before you launch the app you built with AI.