#2 of 10critical

Secret keys shipped to the browser

A secret key in your frontend code is public: every visitor downloads it with your JavaScript, even when it's minified. It usually gets there through a VITE_ or NEXT_PUBLIC_ variable, or an AI tool calling a paid API straight from the page. The fix: rotate the key now, then move the call that needs it into a server or edge function.

Where it shows up: Any frontend (common when an AI tool puts a key in a .env file the build exposes)

Last updated

Are you affected? The 60-second check

Build your app, then search the output for the prefixes that secret keys start with:

Terminal
grep -rEo 'sk_live_[A-Za-z0-9]{6}|sk-proj-[A-Za-z0-9]{6}|sk-ant-[A-Za-z0-9]{6}|sb_secret_[A-Za-z0-9]{6}' dist/ .next/static/ 2>/dev/null

Any match is a live secret in your public code. On a deployed site you can do the same in the browser: open DevTools → Sources and search all files (Cmd+Option+F, or Ctrl+Shift+F) for sk_live_, sk-proj- or service_role. Shippable checks for more than a dozen key formats at once: npx shippable-dev scan localhost:5173.

What it is

Everything in your frontend code is downloaded by every visitor. When a secret key ends up there, it's public, even if it's hidden in a minified file.

It usually happens when a key is put in a variable the build tool inlines into the browser code (for example anything starting with VITE_ or NEXT_PUBLIC_), or when an AI tool calls a paid API directly from the page to make a feature work.

How often we see it

We ran Shippable's static checks on about 1,460 public GitHub repos built with five AI tools (October 2026). Each figure is the share of that builder's repos where the check fired at least once. A dash means it wasn't among that builder's most common findings. Full method and breakdown: Which AI builder ships the safest apps?

FindingBoltLovableCursorReplitv0
Hard-coded fallback secret–5.8%6.3%10%5.4%
Hard-coded secret in source––3.2%4.1%2.7%

What an attacker sees

A Supabase service_role key skips every security rule in your database: full read and write access to everything. A Stripe secret key can issue refunds and read customer data. OpenAI and Anthropic keys let anyone run up your bill.

How to fix it

Paste this into your AI tool:

Paste the prompt below into your vibecoding tool

Security fix: a secret API key is exposed in my frontend code. Move every call that needs a secret key into a server-side function (for example a Supabase Edge Function or an API route), read the key there from a server-only environment variable, and remove it from all client code and from any variable prefixed with VITE_ or NEXT_PUBLIC_. I will rotate the key myself.

Or by hand:

  1. Rotate the key first: the old one has been public and must be treated as stolen. For Supabase, go to Settings → API. For Stripe, Developers → API keys.
  2. Move the code that uses the key to a server or edge function, and keep the key in a server-only environment variable.
  3. Re-scan to confirm the key no longer appears in your site's code.

Vite apps (Lovable, Bolt)

Anything prefixed VITE_ is copied into the browser bundle at build time. Only the Supabase URL and anon (publishable) key belong there. Put other secrets in Supabase Edge Function secrets and call the function from the page.

Next.js apps (v0, Cursor)

Anything prefixed NEXT_PUBLIC_ ships to the browser. Keep secrets in plain environment variables and use them only in route handlers, server actions or server components.

Calls to OpenAI, Anthropic or Stripe from the page

There's no safe way to call these APIs from the browser with your own key. Add a small server endpoint that checks the user is signed in, applies a limit, and makes the call itself.

Mistakes that look fixed but aren't

  • Deleting the key from the code without rotating it. Copies already exist in deploy caches, browser caches and your git history.
  • Moving the key into .env but keeping the VITE_ or NEXT_PUBLIC_ prefix, so the build still inlines it.
  • Encoding or splitting the key so it "looks" hidden. If the browser can rebuild it, so can anyone.
  • Leaving a fallback like process.env.SECRET || "sk_live_..." in shared code.

Check your app

Shippable reads the JavaScript your site serves and looks for the known formats of more than a dozen secret key types. It shows you only a masked version of each key it finds, like sk_liv…a1b2.

Scan your dev server before you push. No account needed:

Terminal
npx shippable-dev scan localhost:5173

Or scan your live app in the browser:

FAQ

Is it safe to put API keys in VITE_ or NEXT_PUBLIC_ variables?

Only for keys that are meant to be public, like the Supabase anon key or a Stripe publishable key. Everything in those variables is copied into the JavaScript every visitor downloads.

My service_role key was in the frontend. What do I do first?

Rotate it in Supabase → Settings → API right away, since it bypasses every RLS policy. Then move the code that needed it into an edge function, and check your database for changes you didn't make.

Does minifying or obfuscating my code hide the key?

No. Minified code is still plain text, and the key has to be readable for the browser to use it.

Is the Stripe publishable key a secret?

No. Keys starting with pk_ are meant for the browser. Keys starting with sk_ or rk_ are secret and must stay on the server.

Related questions