A VITE_ variable is public the moment you build
The concrete launch risk is simple: any environment variable named with a VITE_ prefix is baked into the JavaScript that every visitor downloads. If you named a real secret VITE_OPENAI_KEY or VITE_SUPABASE_SERVICE_ROLE_KEY, that secret is sitting in your client bundle in plain text, readable by anyone who opens the deployed URL. It is not protected by a login, a server, or an environment file on the host. It shipped with the page.
This is the part that surprises people: it is not a leak or a misconfiguration. It is the documented behavior of the prefix. The VITE_ prefix exists specifically to mark a value as browser-visible, so Vite deliberately includes it in the client build. A value without the prefix stays out of the bundle; a value with it goes in. The danger is naming something that should stay server-side with a prefix whose entire job is to make values public.
- Observable from outside: a VITE_-prefixed value is embedded in the built JavaScript and visible in any browser that loads the page.
- Distinct from a runtime server secret: there is no server step that keeps a VITE_ value private. The prefix opts the value into the browser bundle at build time.
- This is an exposure of secret material (CWE-200, information exposure), not a broken login. The value is doing exactly what the prefix tells it to.
Why globalThis._importMeta_.env.VITE_ is replaced at build time
Vite does not read your environment at runtime in the browser. There is no process.env in the browser, and there is no server round trip to fetch the value. Instead, during the build, Vite performs a static text replacement: every reference to globalThis._importMeta_.env.VITE_SOMETHING in your source is replaced with the literal value of that variable, inlined directly into the output JavaScript.
So if VITE_API_KEY is set to a real key when you run the build, the string globalThis._importMeta_.env.VITE_API_KEY in your code becomes the actual key string in the compiled bundle. The variable name disappears; the value remains, hardcoded into a file that ships to the client. There is no later stage that strips it out. This is why a VITE_ value is public the moment you build, not the moment someone finds it.
That mechanic collides with how AI tools scaffold apps. A prompt like "call OpenAI from my app" or "connect to Supabase" produces working code on the first try, and the fastest way to make a key reachable from the client SDK is to put it in a VITE_-prefixed variable and read it with globalThis._importMeta_.env. The app works immediately, the demo call succeeds, so the build looks finished. The step that does not happen automatically is the review where someone asks whether that key was ever supposed to be in the browser.
- AI-generated names like VITE_OPENAI_KEY, VITE_STRIPE_SECRET_KEY, or VITE_SUPABASE_SERVICE_ROLE_KEY put real secrets behind the one prefix that guarantees browser exposure.
- The call working from the client is what hides the problem: it works because the secret is right there in the bundle.
- A working build is not evidence the key is private. It is the opposite.
Lovable, and other tools that build on Vite, apply this same inlining. Anything they scaffold with a VITE_ prefix is inlined into the client bundle exactly as described here. The prefix behavior is a property of Vite, not of any one builder, so it travels with the template.
Check your built bundle for the value
You can confirm this on your own app without any attack. The goal is to look at what actually shipped, not what your source intended.
Build the app the way you deploy it, then search the output. Run your production build and look in the generated assets directory (commonly dist) for the compiled JavaScript. Search those files for the secret value itself if you know it, and separately for the string VITE_ and for telltale key shapes like sk- for OpenAI or service_role for a Supabase key. On the live site, open the deployed URL in a clean browser, open DevTools, and search the loaded scripts in the Sources tab, or watch the Network tab as the page fetches its JavaScript and scan those files.
Keep the deeper DevTools walkthrough light here. Finding a specific key live in the Network tab is its own topic; this note is about the prefix mechanic that put it there.
- Suspicious result: the literal value of a real secret appears in a built .js file, or a string like sk-... or a service_role JWT is sitting in the bundle. That key is public.
- Safer result: only values that are genuinely meant to be public appear — a Supabase project URL, a publishable or anon key, a public analytics token. Those are designed to ship.
- The presence of the VITE_ name in your source is the cue to check; the presence of the value in the built file is the confirmation.
Keep real secrets off the client
The rule is short: only truly public values get a VITE_ prefix. A Supabase project URL, a Supabase anon or publishable key, a public PostHog token, a Stripe publishable key — these are designed to be seen and belong in VITE_ variables. Everything that grants real privilege stays server-side.
For a real secret, the client should never hold the key at all. Move the privileged call behind a server route, a serverless function, or an edge function that holds the secret in its own runtime environment and is never inlined into the browser build. The browser calls your endpoint; your endpoint calls the third party with the secret. The key stays on infrastructure you control, where a VITE_ prefix never touches it.
- Rename and relocate: a variable that holds a secret should lose the VITE_ prefix and move to server-side code, not stay in the client with a different name.
- Route privileged calls through a backend you control rather than calling OpenAI, Stripe, or a Supabase service_role path straight from the browser.
- Any secret that has already shipped in a build must be treated as compromised. Rotate it at the provider, then rebuild and re-check the bundle.
Moving the key server-side stops the next build from shipping it. It does nothing about a key that already shipped. Once a value is in a deployed bundle, you cannot know who copied it, so the only safe assumption is that it is burned. Rotate first, then fix the prefix.
Where VibeCodeGuard fits, and what it does not prove
VibeCodeGuard's Launch Check inspects the built JavaScript on your public surface and flags strings that match known secret patterns embedded in the client code. If a key shape that should never be in a browser shows up in your bundle, the scan reports it with severity and fix direction, so it surfaces in your own check rather than in someone else's traffic. It looks at exactly what an outside visitor can download.
What it cannot do is tell you whether a flagged key was already scraped. A string in the bundle is public, and the scan sees the exposure, not the history. So treat any real secret it finds the same way you would treat any leaked credential: rotate it, do not just remove it from the next build.
A clean Launch Check means no known secret-shaped strings were found on the public surface — not that the app holds no secrets safely. Pattern matching can miss a custom or unusual key format, and it cannot read your server-side code. Treat a flagged key as a launch blocker and a rotation trigger; treat a clean result as one signal, not a guarantee. Next step: build your app, search the output for VITE_ and for your secret values, move any real secret server-side, rotate what already shipped, then run a focused Launch Check against the public URL before you share it.
> launch check
Scan the public surface before launch.
Get severity, evidence, and practical fix guidance for the checks VibeCodeGuard can run from the outside.