If your frontend reads an AI or payments key from import.meta.env.VITE_SOMETHING, that key is in the JavaScript bundle every visitor downloads. Same for NEXT_PUBLIC_* in Next.js and REACT_APP_* in Create React App. Putting it in a .env file changes where it's stored on your laptop, not where it ends up.
You can check in ten seconds: open your deployed site, open DevTools → Sources, and search the bundle for your key's prefix (sk-, AIza, sk_live_).
Automated scanners crawl deployed sites and public repos for those prefixes. Developers have described the results publicly this year on Google's own forum, for example about $6,000 in roughly 40 minutes from an AI Studio sample exported to Vite, and about $2,200 from a VITE_GEMINI_API_KEY used as a shared fallback.
Providers have added protections (Google now rejects unrestricted Gemini keys and offers project spend caps; OpenAI has project limits), but those work per project or account, not per key, and they don't stop the key from being copied.
1. A small backend endpoint. A serverless or edge function holds the key as a secret; your frontend calls the function. Free, and the right answer if you'll also add rate limiting and per-user budgets.
2. The provider's own client-side option, if it has one. For example, Firebase App Check with Firebase AI Logic for Gemini on mobile and web.
3. An edge proxy with per-key limits. You store the real key once and your app sends a stand-in token instead. The proxy adds the real key and forwards the call, and each token carries its own rate limit, allowed endpoints, spend cap and an off switch. I build one of these, KeyVault Edge. You can watch it allow one request and block two misuse attempts on a live demo, no signup.
One honest limit for option 3 (and for any token a browser can see): the token can still be copied and replayed from a server. The point is that a leaked token has hard limits and can be revoked in one click without rotating the provider key, so the damage is bounded.
VITE_, NEXT_PUBLIC_ or REACT_APP_ variable
How did your key end up in the frontend? I'd like to hear what made it the easiest option at the time.