VibeCheck

Guide · API keys

Exposed API keys in frontend code

Some keys are meant to be in your frontend. Others give full access to your database, your Stripe account or your AI bill. The variable name won't tell you which is which, so here is how to tell.

Everything with a public prefix ends up in the browser

Frontend frameworks only expose environment variables with a specific prefix, and they do it by copying the value into the JavaScript bundle at build time:

FrameworkPrefix copied into the bundle
Vite (Lovable, Bolt, most React apps)VITE_
Next.js (v0 and others)NEXT_PUBLIC_
Create React AppREACT_APP_
ExpoEXPO_PUBLIC_

Once a value is in the bundle, every visitor downloads it. Minification does not hide it, and removing the variable later does not remove it from copies already downloaded, cached or archived. A secret that was ever deployed this way should be treated as leaked.

Keys can also reach the frontend without any prefix: pasted straight into a component, or written into a config file that the frontend imports. AI builders do this when asked to "just make it work".

Keys that are designed to be public

These are safe in the frontend as long as the service behind them is configured correctly:

Keys that must never be in the frontend

KeyLooks likeWhat someone can do with it
Supabase service_role / secret keyJWT with "role":"service_role", or sb_secret_...Read and change every table, bypassing RLS
Stripe secret / restricted keysk_live_..., rk_live_...Read customers and payments, issue refunds (within the key's permissions)
Stripe webhook secretwhsec_...Forge "payment succeeded" events to your server
OpenAI / Anthropic / other AI keyssk-..., sk-ant-...Spend your credits
Email providersResend re_..., SendGrid SG....Send email as your domain
Cloud and code hostingAWS AKIA... + secret, GitHub ghp_...Depends on permissions, often a lot
Database URLs with passwordspostgres://user:pass@...Connect to the database directly

Spotting a service_role key

Older Supabase keys are JWTs: three base64 parts separated by dots, beginning with eyJ. The anon key and the service_role key look almost identical. Decode the middle part locally (for example echo 'MIDDLE_PART' | base64 -d; add = padding if it complains) and look at the role field. anon is fine; service_role is not. Avoid pasting real keys into online decoders.

How to check your own app

  1. Open your live site, then the browser's developer tools.
  2. In the Sources (Chrome) or Debugger (Firefox) panel, search across all files for sk_live, sk-, sb_secret, service_role, whsec_, re_, postgres://.
  3. Or build locally and search the output: grep -rE "sk_live_|sk-ant-|sb_secret_|whsec_" dist/
  4. For every eyJ... string, decode it and check the role.

If a secret key leaked

  1. Rotate it first. Create a new key at the provider and revoke the old one. Removing it from code does nothing for copies already out there. For Supabase, rotating the legacy JWT secret also changes the anon key, so plan to redeploy the frontend; projects on the newer API keys can revoke a single secret key.
  2. Check for misuse: Stripe logs, AI usage dashboards, Supabase logs, email sending history.
  3. Move the code server-side. Put the secret in your backend's environment (for example Supabase edge function secrets, set with supabase secrets set) and have the frontend call that function instead.
  4. Check the function too: it should verify the user and apply limits, or it just becomes a public proxy for your key.
  5. Clean git history if the repository is or might become public. The rotation is what actually protects you; the history cleanup just reduces noise.
Rule of thumb: if the provider's docs call it "publishable", "public", "anon" or "client", it was designed for browsers, and the protection lives in rules or restrictions you configure. If they call it "secret", "service", "server" or "private", it must stay on a server.