---
title: "Secret keys shipped in the frontend"
slug: secret-keys-in-the-frontend
severity: critical
category: Secrets
summary: "A Supabase secret key, Stripe secret key, or model API key in the browser bundle is public. Anyone can copy it and use it as your server."
platforms:
  - Supabase
  - Next.js
  - Vercel
  - Stripe
  - Firebase
updated: "2026-10-13"
related:
  - supabase-tables-without-rls
  - secrets-committed-to-git
  - exposed-env-and-debug-endpoints
  - unbounded-llm-spend
---

Anything bundled into a web page can be read. That includes JavaScript, source maps, and environment variables that the framework inlines because their names start with `NEXT_PUBLIC_`, `VITE_`, or a similar public prefix.

Supabase's API key guide draws the line by where the code runs. Code you ship (browser, mobile app, CLI) gets the publishable key. Code you control (server, Edge Function, cron) may use the secret key, because that key bypasses row level security. The legacy `service_role` key is the same class of secret. Supabase also says a secret key must never appear in a browser, including on localhost, and that their API returns 401 when a secret key is sent with a browser user agent. That block is not a reason to ship the key. A request from curl or a script is not a browser.

The same rule covers a Stripe secret key, an OpenAI or Anthropic key, a Firebase service account, and a database connection string.

## How to check in 5 minutes

1. Search the repo for `service_role`, `sb_secret_`, `SUPABASE_SERVICE_ROLE`, `sk_live_`, `sk_test_`, `BEGIN PRIVATE KEY`, and `api.openai.com` keys. Then search for those strings next to `NEXT_PUBLIC_` or `VITE_`.
2. Open the built site, view source, and search the JavaScript files for `sb_secret_`, `service_role`, and `sk_live_`. A production build is the honest check. Dev mode can hide things the bundler will still inline.
3. In the browser network panel, look at the `apikey` header on requests to Supabase. It should be the publishable key, not the secret key.
4. Check framework config. In Next.js, only variables prefixed with `NEXT_PUBLIC_` are exposed to the browser. If the secret uses that prefix, it is in the bundle.

## How to fix

### Supabase

Keep two variables, matching Supabase's own example:

```bash
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_...

SUPABASE_SECRET_KEY=sb_secret_...
```

Create the browser client with the publishable key. Create an admin client only in a file that is imported by server code, and never from a Client Component. If the secret key has already shipped, rotate it in Settings, then API Keys, and disable the legacy `service_role` key as well. Creating a new secret key does not revoke the old JWT-based key. Supabase's rotation steps say to replace every use first, then retire the compromised key.

### Next.js

Assume every `NEXT_PUBLIC_` value is public. Do not pass a secret as a prop into a client component. Server Actions and Route Handlers can read `process.env.SUPABASE_SECRET_KEY`. A client component cannot, unless you made it public.

### Vercel

Put the secret in the project's environment variables for Production, Preview, and Development as needed. Do not prefix it with `NEXT_PUBLIC_`. Redeploy after rotating. The old deployment keeps the old value until it is replaced.

### Stripe and model providers

The Stripe secret key and webhook secret stay on the server. The browser may use a publishable key (`pk_...`). Model keys stay on the server. See [unbounded model spend](/issues/unbounded-llm-spend).

### Firebase

The Firebase web config (apiKey, projectId, and similar) is designed to live in the client. Security rules are the control. A service account JSON file is not the web config. It must not be imported by frontend code.

## Agent prompt

```text
You are removing secrets from client-side code. Do not add features.

Search this repo for service_role, sb_secret_, SUPABASE_SERVICE_ROLE, Stripe secret keys (sk_live_ and sk_test_), webhook secrets, and LLM provider keys.

Rules:
1. Any variable prefixed NEXT_PUBLIC_ or VITE_ is public. A secret must not use that prefix.
2. The Supabase browser client may use the publishable key only. The secret key may appear only in server modules that client components do not import.
3. If a secret was exposed, do not only rename the variable. Tell me to rotate it in the provider dashboard, including the legacy Supabase service_role key if that was the value that leaked.
4. Do not print secret values in logs, comments, or the pull request description.

Show every file you changed and the env var names the host needs. Do not include the secret values.
```

## FAQ

### The anon key is in my JavaScript. Is that a leak?

The legacy anon key and the publishable key are low-privilege identifiers. Supabase says they are safe to expose if row level security is on and you review policies for the `anon` and `authenticated` roles. Treat `service_role` and `sb_secret_` as passwords.

### Supabase blocks secret keys in the browser. Can I rely on that?

No. Their docs say the browser block returns HTTP 401, and an attacker can still use the key from other tools. If it reached a browser, rotate it.

## Sources

- [Supabase API keys](https://supabase.com/docs/guides/api/api-keys)
- [Supabase advisors](https://supabase.com/docs/guides/database/database-linter)
