OpenAI key exposed in a Lovable app — the cost and the cure
Chat and completion features push an OpenAI key straight into a Lovable bundle. Detect exposure in seconds and move the call behind a function.
You added AI features to your Lovable app. It works — and somewhere in the built JavaScript sits a literal OpenAI key that anyone can lift and spend.
Here is the mechanics of how it got there, what abuse looks like from your invoice, and the proxy pattern that ends both.
What actually happened
The feature request was "add chat". The shortest implementation called OpenAI directly from a component, with the key read from a VITE_ variable. Nothing errored on the way — that is what separates this failure from the ones your tooling catches.
- The component imported a client and passed
import.meta.env.VITE_OPENAI_API_KEY. - Vite substituted the literal value during build — the variable name survives as a hint, the value as plaintext.
- Crawlers that scan served JavaScript match
sk-proj-shapes as a matter of course; publication is discovery. - Spend follows: model calls billed to your org, rate limits burned against your projects, no ceiling unless you set one.
Every step above is individually reasonable and none of them prints a warning. The value crosses into the bundle during substitution, not execution, so nothing in your runtime ever sees the moment it happened.
What someone can do with it
Billable API access with your name on the invoice:
- Model calls across every model your org has enabled.
- File and assistant endpoints where plan allows.
- With an admin-key variant, the ability to mint additional keys before you notice (see the admin-key page).
Rotate first
Rotate before you touch any code. The key has been served to every visitor since the deploy that introduced it, cached by CDNs along the way, and very likely harvested by crawlers that scan served JavaScript for exactly these shapes. At platform.openai.com/api-keys: revoke the exposed key, create a scoped replacement, then update the server-side consumer. Removing the string from source does not un-publish it; only revocation closes the door.
A rotated key left in old deploys is still discoverable in CDN caches and archived copies. Rotation plus redeploy closes both halves; either alone leaves the door ajar.
Move the call somewhere the browser cannot read
The structural fix is always the same shape: the call moves to a context that holds the key without serving it, and the browser asks your server instead.
// src/components/Chat.tsx (client)
const openai = new OpenAI({
apiKey: import.meta.env.VITE_OPENAI_API_KEY,
});
await openai.chat.completions.create(body);
// supabase/functions/chat/index.ts (server)
Deno.serve(async (req) => {
const openai = new OpenAI({ apiKey: Deno.env.get("OPENAI_API_KEY")! });
return Response.json(await openai.chat.completions.create(await req.json()));
});
Check whether yours is exposed
You can check manually right now: open the site, view source or open DevTools, and search the built JavaScript for sk-proj-. A hit means the string shipped; decode or prefix-check it before deciding how bad the news is.
Manual searching proves one page on one day. KeyDrift fetches the deployment the way a browser would, follows chunks named only in route manifests, reads streamed hydration payloads, and classifies what it finds — secret, or public-by-design — so an anon key never shows up dressed as an emergency.
Keep it from coming back
One more thing worth knowing before you close the tab: fixing this once does not end the story. Agents imitate whatever pattern is already in the repo, templates carry their own defaults, and the next feature request can reintroduce the same shape of code. Continuous monitoring re-scans every deploy and alerts only on change — new, regressed, resolved — so the comeback attempt is a notification instead of a quarter-end surprise.
There is no test mode to hide behind
Stripe gives you sk_test_; OpenAI gives you nothing comparable. Every OpenAI key is live money, which moves this failure from "hygiene" to "spend" the moment it publishes. Spend caps reduce blast radius after the fact; they are not a substitute for keeping the key server-side.
The bigger picture
Zoom out and the pattern is bigger than one repo. AI-assisted output has outgrown review capacity everywhere at once, which means thousands of teams are making the same reasonable-looking tradeoffs in the same week. Lovable users are not uniquely exposed — they are typically exposed. The failure mode documented above is the modal outcome of velocity without verification, not evidence of carelessness.
How KeyDrift reports this exact finding
Report anatomy matters during incidents, so it is worth reading once calmly: masked string (never the live value — it ceases to exist outside the detection engine), salted fingerprint (trackable within your workspace, useless to strangers), chunk path (your starting point for a "git log -S" hunt), disposition (secret versus public-by-design), confidence (matches below 0.5 never reach the page at all).
Manual check, step by step
The full manual drill, for readers who want zero dependence on any tool: open the deployed site in a private window; launch DevTools → Sources; use Search-all-files (Ctrl/Cmd+Shift+F) for sk-proj-; then repeat for the other marker families — eyJ, sk_live_, sk-proj-, AKIA, postgres, BEGIN PRIVATE KEY. Decode anything JWT-shaped before reacting, and classify public-by-design formats as expected guests rather than intruders.
Close the loop with monitoring
If you take one operational step from this page, make it this: put the URL under continuous monitoring (free tier covers one project daily). The first scan tells you whether you have a problem today; the schedule tells you whether the problem comes back next month after someone re-adds the convenient line.
Common questions
Will OpenAI alert me about unusual usage?
Assume not. Usage dashboards show spikes after the fact; treat detection as your job, via scan or bill alarm.
Can I keep the call client-side with a restricted key?
OpenAI has no browser-restricted key class. The proxy pattern is the supported architecture.
Run a free scan at keydrift.dev/scan — paste a URL or the bundle source itself, no account. Findings arrive masked, with the exact chunk they live in.
Related
KeyDrift is an independent product and is not affiliated with, endorsed by, or sponsored by Lovable. The name is referenced descriptively.