Skip to content
ShipSecurelySecurity reference
Menu
Your tool here — sponsor ShipSecurely

Issues

Secret keys shipped in the frontend

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.

Share link · Share on X

Last updated

criticalSecrets

Shows up in: Supabase, Next.js, Vercel, Stripe, Firebase

Markdown version

What it is

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:

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.

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.

Copy-paste prompt

Paste this into Cursor, Claude Code, or whichever agent is editing the repo. Read the diff before you merge it.

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.

Questions

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

Related