KeyDrift
Free scan
← Leak scenarios

The Lovable/Vite secret-leak mechanism, precisely

VITE_ prefixes tell Vite to inline values into the bundle at build time. Understand static substitution end-to-end, then verify what shipped.

5 min read

Most secrets in Lovable deployments arrive through one door: a variable named so the bundler publishes it. Not stolen, not leaked by a bug — printed, by design, at build time.

This page models the substitution exactly, shows the variations teams fall into, and gives you the thirty-second check that settles what your build really contains.

The mechanism, precisely

Vite resolves import.meta.env statically. There is no runtime lookup to protect you, because there is no runtime environment in a browser:

  1. During build, Vite scans modules for import.meta.env.VITE_* references.
  2. Wherever one appears, the entire expression is replaced by a JSON literal of the value.
  3. Minification renames your variables but cannot touch string literals it depends on.
  4. The result ships: identical bytes to every visitor, greppable by anyone.
// source (safe-looking):
const cfg = { key: import.meta.env.VITE_STRIPE_KEY };
// built output (served):
const cfg = { key: "sk_live_FAKE000000000000000000000" };

Substitution is static and total: the literal replaces the reference everywhere the prefixed name appears, in every chunk, before anything is minified. Obfuscation changes what the string looks like; it does not change who can read it.

Variations of this mistake

  • Renaming a secret’s variable with a VITE_ prefix to silence a build error — the rename is the leak.
  • Believing .env.production differs from .env.local; both feed the same substitution when prefixed.
  • Passing secrets through define() in vite.config — same inlining, quieter name.

Where the value belongs instead

Values that must stay secret belong behind an endpoint: an Edge Function or any serverless function your host offers, holding the key in server-side env config.

Verify what your build actually ships

Search the deployed JavaScript for VITE_ in DevTools — thirty seconds, no tools beyond the browser. What you find is ground truth; everything earlier in the pipeline is intent.

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.

Why snapshots are not enough

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.

The bigger picture

It helps to name the economics honestly. Fixing this class of leak costs minutes when caught at deploy time and days when caught at invoice time, because by then the credential has been harvested, validated, resold or drained — often all four. Detection latency is the entire game, which is why the monitoring half of KeyDrift exists alongside the scanning half.

How KeyDrift reports this exact finding

When KeyDrift finds this on your deployment, the report shows a masked value (first 8 and last 4 characters only), a salted fingerprint for tracking, the exact chunk filename carrying it, and a severity with written rationale. Public-by-design neighbours — anon keys, publishable keys, Firebase web constants — appear as informational context rather than noise, because knowing what should be there is what makes the real findings credible.

Manual check, step by step

A five-minute version you can run anywhere: view-source on the landing page, copy every src= script URL, fetch each and search the results for VITE_. It misses manifest-only chunks and streamed payloads — which is precisely the gap between "I checked" and "it is clean" — but it catches the loud majority and builds the pattern-recognition that makes scanner output legible.

Close the loop with monitoring

Monitoring closes the loop that one-time verification leaves open. A scheduled scan refetches everything, diffs against history, and fires only on transitions: created, regressed, resolved. Regression alerts matter most here — they fire when a previously fixed finding returns, which in agent-era codebases is less a possibility than a schedule.

What the plans change

  • Free $0 — 1 project · daily scans · email alerts · findings always visible.
  • Indie $29/mo — 3 projects · hourly · Slack added · 80 chunks per scan.
  • Team $89/mo — 15 projects · every 15 minutes · Discord + webhooks · 150 chunks.
  • Growth — from $249/mo, quoted display-only until checkout ships.

The constant across every tier: plans limit how much is watched, never what a scan found. Visibility is structural, not promotional — asserted by tests over the entitlements model itself.

Common questions

Does .env.local protect me?

Only while values stay unprefixed. Local files never reach git anyway; the bundle is the exfiltration path, not the repository.

Is minification any protection?

No — minification transforms names and whitespace, not string literals the code depends on.

Are any VITE_ variables legitimate?

Yes: public configuration like the Supabase URL or anon key. Prefixes mark publicity; choose contents accordingly.


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.


KeyDrift is an independent product and is not affiliated with, endorsed by, or sponsored by Lovable. The name is referenced descriptively.

Published by PostHat, KeyDrift’s content pipeline. Every factual claim is grounded in KeyDrift’s product documentation.