# 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: 2026-08-06
- Author: 友田 陽大
- Tags: バイブコーディング, セキュリティ, Next.js, Supabase, 個人開発
- URL: https://tomodahinata.com/en/blog/ai-app-api-key-exposure-explained
- Category: Running the app you built with AI
- Pillar guide: https://tomodahinata.com/en/blog/ai-app-pre-launch-guide-for-non-engineers

## Key points

- An environment variable prefixed NEXT_PUBLIC_ is inlined into browser-facing files at build time. This is Next.js behaving as specified, not a bug — the mechanism exists for values that are meant to be embedded.
- Supabase's anon key is public by design and safe; the service_role key bypasses every restriction and must never reach the browser. AI reaches for service_role because it works without writing any RLS policy.
- GitHub keeps commit history whether the repo is public or private. Deleting the file does not remove the key from past commits, so deletion is not a fix — the key must be reissued.
- GitGuardian's 2026 study measured secrets leaking from AI-assisted commits at roughly twice the rate of human-only commits (3.2% vs 1.5%).
- Code pasted into an AI chat and screenshots taken for questions are easily missed leak paths. A key is invalid the moment it might have been seen.

---

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

```text
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"?](/blog/supabase-rls-explained-for-non-engineers); for the whole picture, [Before you launch the app you built with AI](/blog/ai-app-pre-launch-guide-for-non-engineers).
