One AI tool blocked more than 17,000 deployments in 30 days for carrying exposed secrets. That number is Vercel’s, about their own v0. This is not a beginner slip: it is what happens by default when you build software with AI.
And most of those keys were sitting in a .env — which is exactly where everyone believes they are safe.
What a .env actually is
A .env is a plain text file with a list of variables. That is all. It is not encrypted, it has no special permissions, and nothing guards it. The only thing it does for you is keep values out of your code so you do not leak them by copy-paste.
A key being in there does not make it secret. What decides that is where it ends up — and there are two completely different paths.
The prefix that changes everything
This is the part almost nobody explains, and it is the part that matters. Build tools follow one rule: only variables carrying a certain prefix get baked into your site. The rest stay out.
| Tool | Published to the browser | Stays server-side |
|---|---|---|
| Vite (Lovable, Bolt, most) | VITE_… | everything else |
| Next.js (v0) | NEXT_PUBLIC_… | everything else |
| Create React App | REACT_APP_… | everything else |
Read that backwards, because that is where it hurts: if your variable carries that prefix, its value is written inside the JavaScript that anyone opening your site downloads. It is not theoretical and nobody has to "hack" anything: it is in the file, in plain text, in the open.
And you did not add that prefix for fun — the AI added it because without it the variable does not work in the browser. So the code that "finally worked" is precisely the code that publishes your key.
Check it yourself, in two minutes
No tools needed. On your already-published site:
- Open your site and hit Ctrl+U (Cmd+Option+U on Mac) to view source.
- Find the
assetsfolder and open any.jsfile. - Hit Ctrl+F and search for a chunk of your key — the first eight characters are enough.
- If it shows up, it is published. End of discussion.
Try this in the address bar too: yoursite.com/.env. If it downloads a file instead of erroring, your whole .env is on the internet — which happens when the project folder gets uploaded unfiltered and the file travels with it.
Not every key is a problem
This is where most advice you will find overshoots. Some keys are designed to be public, and seeing them in your code does not mean you are in danger:
- Public by design: Supabase’s
anonkey, a Google Maps API key, Stripe’s publishable key (pk_…), an analytics token. They go to the browser on purpose — their protection is not secrecy, it is the restrictions you put on them. - Must never ship: Supabase’s
service_role, any Stripesk_…, OpenAI or Anthropic tokens, database credentials, AWS keys. These grant full access, with no limit and no record of who used them.
The short rule: if the name contains secret, service, private, or starts with sk_, it never goes to the browser. If it says public, anon or publishable, it can — but go configure its restrictions.
Restricting the ones that are public
An unrestricted public key is a key someone else can spend on your behalf. Two things that take five minutes:
- Domain restriction. Google Maps and nearly every service lets you say "this key only works when the request comes from
my-site.com". Without it, someone copies it and your bill grows on someone else’s traffic. - Data access rules. In Supabase, the
anonkey is safe *only* if you turned on Row Level Security. Without RLS that public key reads your entire database. It is the most expensive mistake on this list, and no scanner catches it: you have to turn it on.
If you already published one
It happens. What matters is the order — and the first step is not the one people do first:
- Rotate the key first. Go to the service, generate a new one, disable the old one. While the old one is alive, deleting it from your code does nothing.
- Then fix the code. Take the value out, use the variable, publish again.
- Check your Git history. If the
.envwas ever committed, it is still in there even if you deleted it afterwards. A public repository with history is a key given away. - Check the service’s usage. If someone used it, it shows — odd spikes, requests from countries where you have no users.
What FluentOps checks, and what it does not
On every publish, on every plan — the free one included — it reads what you are about to publish and what ended up published:
- Secret files (
.envand friends): in your source it is a warning; on the published site it is high severity, because it is downloadable right now. - Keys written into the code: AWS access keys, private key blocks, JWTs, credentials inside a URL.
- Exposed source: a downloadable
.gitfolder, and *source maps* — your original unminified code, there for the taking. - Vulnerable dependencies: on Node it runs
npm auditacross the full tree, direct and transitive. One real project showed 1 vulnerability when reading the manifest alone; the full tree had 3.
And the limits, because overpromising on security is worse than not checking: secret values are always redacted, text files are read (binaries are skipped), there is a cap of 50 findings, and outside Node the dependency scan covers direct dependencies. None of this replaces turning on RLS or restricting your public keys — that part is yours.
What is actually different
Other platforms look for secrets too: Netlify fails your build when it finds one, and Vercel blocks v0 deployments for the same reason. Comparing feature by feature does not say much. What does say something is the whole set, and above all what happens when you hit the free plan’s ceiling:
- Checking your keys in the code and in what you published, at different severity depending on where they are.
- Telling you whether your
.gitfolder or your *source maps* ended up downloadable. (Vercel lets you protect source maps; that is a different thing from telling you that you shipped them.) - Checking your vulnerable dependencies on every deploy, across the full tree. Neither of the two does this out of the box: that lives in integrations you install yourself.
- Generating the technical SEO your site was born without — sitemap, robots, OG, description, favicon — with no plugin to install.
- And not switching your site off. Netlify pauses every site on the account when the month’s credits run out; Vercel’s Hobby plan pauses when you hit the cap.
That last point is what holds the word "free" up. A platform that checks your code but switches your site off when credits run out is not giving it to you for free: it is lending it to you. Name another that does all five.
There is nothing to configure and nothing to enable. If you have never in your life run npm audit, this is the first thing that will tell you.
And if your project is not online yet, here is how to publish it — with the check included from the very first deploy.