Skip to main content
友田 陽大
Running the app you built with AI
バイブコーディング
セキュリティ
Next.js
Supabase
個人開発

NEXT_PUBLIC_ and service_role — why the "API key" AI wrote is visible to everyone else

How AI-generated code ends up leaving API keys and passwords readable by anyone, explained for non-engineers. What NEXT_PUBLIC_ actually does, the decisive difference between Supabase's anon and service_role keys, and why a key ever pushed to GitHub must be reissued. Includes checks and an instruction for your AI tool.

Published
Reading time
8 min read
Author
友田 陽大
Share

"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.

  1. Open the app in a browser
  2. Press F12 (⌘+Option+I on Mac) for the developer tools
  3. 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 keyservice_role key
IntentPublic by designAbsolutely secret
RLSSubject to itBypasses all of it
In the browser✓ normal✗ catastrophic
UseOrdinary app accessServer-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 anon key → nothing comes back until the RLS policies are written correctly, so you get an error
  • Using the service_role key → 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 anon key → fine (assuming RLS)
  • Supabase service_role key → 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 anon key is public by design and safe. The service_role key bypasses everything and must never be in the browser
  • AI reaches for service_role because "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.

Frequently asked questions

What does adding NEXT_PUBLIC_ actually do?
In Next.js, when an environment variable's name starts with NEXT_PUBLIC_, its value is embedded directly into the browser-facing JavaScript at build time. It is not read at runtime — it is written into the file as a literal string, so anyone who opens the page can read it in the developer tools. This is not a bug; it is the sanctioned mechanism for values that genuinely need to run in the browser, such as a public API URL.
My Supabase anon key is visible in the browser. Is that a problem?
No. The anon key is public by design and being readable in the browser is the normal state. That key is subject to Row Level Security, so as long as RLS is configured correctly nobody can fetch another user's data with it. The dangerous combination is a public anon key with no RLS.
What makes the service_role key different?
The service_role key is an administrative key that bypasses every restriction, RLS included. If it leaks, configuring RLS perfectly buys you nothing — reading, rewriting and deleting every table all become possible. It belongs only in a server-side environment variable and may only be used from server-side code.
Why does AI want to use the service_role key?
Because no error appears. With the anon key, data cannot be fetched until the RLS policies are written correctly, so you get an error. With the service_role key everything works from the start without writing a single policy. AI targets 'it runs', so it takes the shortest path that runs — and here, running and being safe point in opposite directions.
I pushed a key to GitHub once. Is deleting the file enough?
It is not. GitHub retains commit history whether the repository is public or private, so even after you delete the file the key is still readable in an earlier commit. The fix is to revoke the key in the provider's dashboard and issue a new one. Only once it is revoked does the item count as handled.

References

友田

友田 陽大

Developer of a METI Minister's Award–winning product. With TypeScript + Python + AWS, I deliver SaaS, industry DX, and production-grade generative AI (RAG) end to end — from requirements to infrastructure and operations — single-handedly.

“It works. But is it safe to launch?”

Twenty questions, no signup — a pre-launch self-check for the app you built with AI

You don't need to read any code. Answer yes / no / not sure to questions like “can only the person who logged in see their own data?” You get your risks ordered by severity, plus a fix instruction you can paste straight into Claude Code or Cursor. Nothing you type leaves your browser.

Available for both project-based (contract) and advisory engagements. Start with a free 30-minute consult.

最短ルート:カレンダーから直接予約

相談内容が固まっている方は、フォーム送信よりその場で日程を確定する方がスムーズです。下記から空き時間をお選びください。

  • 30分のオンライン無料相談
  • Google Meet / Zoom / Microsoft Teams
  • NDA 商談前締結可・無理な営業はいたしません
無料相談の空き枠を予約する

Also worth reading