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:
| Framework | Prefix copied into the bundle |
|---|---|
| Vite (Lovable, Bolt, most React apps) | VITE_ |
| Next.js (v0 and others) | NEXT_PUBLIC_ |
| Create React App | REACT_APP_ |
| Expo | EXPO_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:
- Supabase anon key / publishable key (
sb_publishable_...). Its access is limited by Row Level Security. If RLS is off, the anon key can read your tables, but the problem is the missing RLS, not the key. See the RLS guide. - Firebase web config, including
apiKey(AIza...). It identifies your project; access is controlled by Firebase Security Rules. Restrict the key in Google Cloud to the APIs you use. See the Firebase rules guide. - Stripe publishable key (
pk_live_...,pk_test_...). It can create tokens and start checkout, not read your account. - Map keys meant for browsers: Mapbox public tokens (
pk.) and Google Maps browser keys. Restrict them to your domain so others cannot use your quota. - Analytics and error tracking: PostHog project keys, Sentry DSNs and similar ingestion-only keys.
Keys that must never be in the frontend
| Key | Looks like | What someone can do with it |
|---|---|---|
| Supabase service_role / secret key | JWT with "role":"service_role", or sb_secret_... | Read and change every table, bypassing RLS |
| Stripe secret / restricted key | sk_live_..., rk_live_... | Read customers and payments, issue refunds (within the key's permissions) |
| Stripe webhook secret | whsec_... | Forge "payment succeeded" events to your server |
| OpenAI / Anthropic / other AI keys | sk-..., sk-ant-... | Spend your credits |
| Email providers | Resend re_..., SendGrid SG.... | Send email as your domain |
| Cloud and code hosting | AWS AKIA... + secret, GitHub ghp_... | Depends on permissions, often a lot |
| Database URLs with passwords | postgres://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
- Open your live site, then the browser's developer tools.
- In the Sources (Chrome) or Debugger (Firefox) panel, search across all files for
sk_live,sk-,sb_secret,service_role,whsec_,re_,postgres://. - Or build locally and search the output:
grep -rE "sk_live_|sk-ant-|sb_secret_|whsec_" dist/ - For every
eyJ...string, decode it and check the role.
If a secret key leaked
- 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.
- Check for misuse: Stripe logs, AI usage dashboards, Supabase logs, email sending history.
- 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. - Check the function too: it should verify the user and apply limits, or it just becomes a public proxy for your key.
- Clean git history if the repository is or might become public. The rotation is what actually protects you; the history cleanup just reduces noise.