Your API key is in your front-end JavaScript. Here is what to do in the next ten minutes.

Any key in your front-end code is public. Not hidden, not obscured, not hard to find: public. Every visitor's browser downloads your JavaScript, and anyone can open it and read the key. If you have just found one, here is the order to do things in, and why rotating the key comes before touching the code.

Can you hide an API key in front-end JavaScript?

No. There is no way to put a secret in code that runs in the visitor's browser and keep it from the visitor. Minifying, encoding, splitting it across variables and fetching it from a config endpoint all end the same way: the browser has the key, so the person at the keyboard has the key. The only fix is to move the call that needs the key to a server you control, and let the browser call your server instead.

This surprises people because AI builders make it look fine. The app works, the key is "in an environment variable", and nothing warns you. The warning is this post.

Step 1: Rotate the key. Right now, before anything else.

Go to the provider's dashboard, revoke the exposed key, and create a new one. Do this before you touch the code, because deleting the key from the code does not un-leak it: it is already in the browser cache of everyone who visited, in your GitHub history, and possibly in a scraper's database. The key is burned. A new one is the only way back.

ProviderWhere to rotate
OpenAI (sk-proj-, sk-)platform.openai.com, API keys: revoke, then create
Anthropic (sk-ant-)console.anthropic.com, API Keys
Stripe (sk_live_, whsec_)Dashboard, Developers, API keys: roll the key
Supabase service_role / sb_secret_Project settings, API: regenerate (this also logs out every server using it)
Resend, SendGrid, Twilio, GitHub tokenseach dashboard's API keys page: delete, then create

If the key was for a service with a spending limit, set or lower it now too. You can raise it again next week.

Step 2: Check what it was used for

Open the provider's usage or billing page and look at the last few days. A spike you did not cause means someone found it before you did. For OpenAI and Anthropic that usually shows as a burst of requests at odd hours. For Stripe, look at the events log for refunds, payouts or customers you did not create. For a Supabase service_role key, assume the database was readable and decide whether your users need to hear from you.

Most of the time nothing happened yet. Scrapers that collect keys from public JavaScript and GitHub run constantly, though, so "nothing yet" has a short shelf life.

Step 3: Move the call to a server

The key needs to live somewhere the browser cannot see, and the call that uses it needs to happen there. Every builder has a place for this. The pattern is the same everywhere: the browser calls your function, your function reads the key from a secret and calls the provider, and the response comes back.

Built withWhere the server-side code goesWhere the key goes
Lovablea Supabase Edge FunctionSupabase secrets (Edge Functions, Secrets)
Bolta Supabase Edge Function or a Netlify FunctionSupabase secrets or Netlify environment variables (not prefixed VITE_)
Replitan Express route in the server folderthe Secrets pane (not a VITE_ variable)
v0 / Next.js on Vercela Route Handler under app/api/Vercel environment variables, without the NEXT_PUBLIC_ prefix
Cursor / Windsurfwhatever API layer the project has, or a serverless functionthe host's secret store, never a file in the repo
Paste into Lovable click to select

Security fix: my OpenAI API key is in the front-end code, so anyone visiting the site can copy it.
Move the OpenAI call into a Supabase Edge Function, store the new key as a Supabase secret named OPENAI_API_KEY, and have the front end call that function instead.
Remove the key from every front-end file and from any VITE_ environment variable. Do not change the UI.

For Replit, swap the middle sentence for: "Move the OpenAI call into an Express route on the server, read the key from process.env.OPENAI_API_KEY set in Secrets, and have the front end call that route." For v0: "Move the OpenAI call into a Route Handler at app/api/chat/route.ts, read the key from process.env.OPENAI_API_KEY, and remove the NEXT_PUBLIC_ version."

Step 4: Clean the public variables

Build tools copy every variable with a "public" prefix into the browser bundle. That is the feature working as designed: the prefix is a promise that the value is safe to publish.

PrefixBuild toolUsed by
VITE_ViteLovable, Bolt, many Replit Agent apps
NEXT_PUBLIC_Next.jsv0, Vercel templates
REACT_APP_Create React Appolder React projects
EXPO_PUBLIC_Expomobile apps
PUBLIC_SvelteKit, AstroSvelte and Astro projects

Go through your environment variables and delete any with those prefixes whose value is a secret. Keep the ones that are supposed to be public: the Supabase URL and anon key, the Firebase web config, the Stripe publishable key (pk_), analytics IDs. Those are public by design, and the rules behind them (Row Level Security, Firebase rules) are what protect the data.

Step 5: Verify it is gone, everywhere

  1. The live app. Publish, then open the app, press F12, Network tab, reload, click the biggest .js file and search for the first few characters of the old key and the new one. Neither should appear.
  2. The repo. On GitHub, search the repository for the old key's prefix. It will still be in history; that is why you rotated. Confirm no current file holds the new one.
  3. The function. Call the feature in the app and confirm it still works. Check the function's logs to see the request land there.

Which keys are safe in the browser and which never are

Safe in the browser (by design)Never in the browser
Supabase anon key (with RLS on every table)Supabase service_role key, sb_secret_ keys
Firebase web config (with rules not in test mode)Firebase service account JSON
Stripe publishable key pk_Stripe secret key sk_, webhook secret whsec_
Google Maps key restricted to your domainOpenAI, Anthropic, Groq, OpenRouter, Gemini API keys
Analytics and error-tracking public DSNsResend, SendGrid, Twilio, AWS, GitHub tokens, database URLs

The test is simple: if someone copying this value could spend your money, read your data or send mail as you, it does not go in the browser.

Why AI builders keep doing this

Because it works. The first version of "add an AI chat" that succeeds is the one where the browser calls OpenAI directly, and the SDK's dangerouslyAllowBrowser flag exists precisely to let that happen. The builder is optimizing for the feature working, and the feature works. Nothing in the loop checks who else can see the key. That is also why this comes back: the next prompt that touches the AI feature can move the key right back. Check after every change, or let something check for you.

Questions people ask

Is my OpenAI key safe if it is in an environment variable?

Only if that variable is read on a server. A variable prefixed VITE_, NEXT_PUBLIC_, REACT_APP_ or EXPO_PUBLIC_ is copied into the browser bundle by the build tool, so the key is public. Environment variables read inside an Edge Function, an Express route or a Next.js Route Handler stay on the server.

Can I just restrict the key by domain instead of moving it?

Only for keys whose provider supports referrer restrictions and that are designed to be public, such as a Google Maps key. OpenAI, Anthropic and Stripe secret keys have no such restriction; anyone with the key can use it from anywhere. Those must move to a server.

I removed the key from the code. Is that enough?

No. The old key is still valid and still in browser caches, your GitHub history and any scraper's copy. Rotate it at the provider. Removing it from the code only stops the new key from leaking the same way.

How did someone find my key so fast?

Automated scanners read public GitHub commits and JavaScript bundles around the clock looking for key patterns like sk-proj- and sk_live_. A key in a public repo is often used within minutes. A key in a live app's bundle takes longer to find but is just as exposed.

Find the keys you don't know about.

VibeProtect reads the JavaScript your live app sends to visitors and the code in your GitHub repo, and shows each leaked key redacted, with the file, the line and the prompt that moves it server-side. Seven days free, no card.