Every framework draws one line, and a wrong guess ships a secret
The concrete launch risk is a single misjudged variable. Every modern frontend framework splits your environment variables into two buckets: ones that get inlined into the JavaScript every visitor downloads, and ones that stay on the server. Put a real secret on the wrong side of that line and it is in the bundle, readable by anyone who opens the deployed URL. No login, no server, no env file on the host protects it. It shipped with the page.
This is the conceptual model behind the prefix-specific notes on this blog. If you want the deep mechanics for one stack, read the dedicated pieces: VITE_ prefix ships your secrets to every browser covers Vite, and NEXT_PUBLIC_ and the accidental secret covers Next.js. This note stays one level up. The point here is the shared model that every framework follows, the per-framework map of where the line sits, and a checklist to confirm which side each of your variables landed on.
The split is the same idea everywhere, even though the syntax differs. Server only vs public environment variables is not a framework quirk you learn four times; it is one rule with four spellings.
- Observable: a public-marked variable is embedded in the built client JavaScript and is readable by any visitor, crawler, or script that loads the page.
- Only possible: whether an exposed string is still a working credential is a separate question. Exposure is certain the moment you build; exploitability depends on the key.
- This is a secrets-management gap (a credential reached a public surface), distinct from an authorization gap. The value leaks regardless of who is logged in.
Why AI-built apps land on the wrong side
The split is invisible from inside a working app, and that is exactly when AI tools generate the mistake. A prompt like call OpenAI from my app or connect to Stripe produces code that runs on the first try. The fastest way to make a key reachable from client code is to mark it public, because an unmarked variable reads as undefined in the browser and looks like a naming bug. The generated fix silences the error and the demo call succeeds, so the build looks finished.
What does not happen automatically is the review where someone asks whether that value was ever supposed to be in the browser. The call working from the client is the thing that hides the problem: it works because the secret is right there in the bundle. A working build is not evidence the key is private. Sometimes it is the opposite.
- Server env vs client env is a distinction the running app never surfaces. Both feel identical until someone reads the shipped JavaScript.
- AI-generated names like a service-role key or a secret key marked with a public prefix put real privilege behind the one marker that guarantees browser exposure.
- Fast deploy means the public URL exists before anyone greps the built bundle for the value.
The line, per framework
Each framework documents exactly which variables reach the client. The marker differs; the rule does not. The following reflects each framework's documented behavior at the time of writing; confirm against your installed version, since prefixes and defaults are configurable.
Vite:
Next.js:
Nuxt (public env vars next nuxt vite share the same Vite engine underneath, but Nuxt adds its own layer):
SvelteKit:
- Only variables prefixed VITE_ are exposed to client code. Vite inlines them into the bundle at build time via globalThis._importMeta_.env.
- Everything without the prefix is build-time or server-time only and is not inlined into the client bundle.
- Only variables prefixed NEXT_PUBLIC_ are inlined into the client JavaScript at build time.
- An unprefixed variable is server-only. Server Components, Route Handlers, Server Actions, and the Pages Router server functions can read it via process.env, and it never leaves the server unless you pass it into client-facing code yourself.
- Configuration lives in runtimeConfig. The public sub-object (runtimeConfig.public) is exposed to the client; everything at the top level of runtimeConfig is server-only.
- Env vars override these at runtime by name: NUXT_PUBLIC_ prefixed vars map into the public object (client-exposed), while NUXT_ prefixed vars without PUBLIC map into the private, server-only keys.
- The client-safe modules are $env/static/public and $env/dynamic/public. Both expose only variables that begin with the public prefix, which defaults to PUBLIC_.
- The private equivalents, $env/static/private and $env/dynamic/private, are server-only and cannot be imported into client-side code. Static means injected at build time; dynamic means read at runtime.
The marker is a promise to the framework, not a judgment about the value. Marking a variable public tells the build to ship it to every browser. The framework does not check whether the contents are actually safe to publish; that judgment is yours, and it is the step the generated code skipped.
Audit which side each variable is on
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. Work through your variables in four steps.
Read the results plainly:
- List every environment variable your app uses, across the .env file, your host's env settings, and your framework config (for Nuxt, the runtimeConfig object).
- Classify each one as truly public or a real secret. A project URL, a publishable or anon key, a public analytics token, and a feature flag are designed to be seen. A secret key, a service-role key, a database connection string, an API key for OpenAI or Anthropic, and any webhook signing secret are not.
- Confirm only the truly-public ones carry a client-exposed marker. For Vite that is the VITE_ prefix; for Next.js, NEXT_PUBLIC_; for Nuxt, placement under runtimeConfig.public (or a NUXT_PUBLIC_ env var); for SvelteKit, the PUBLIC_ prefix and import from a public env module. Anything in the secret column must not carry any of these.
- Grep the built bundle to confirm no secret leaked. Build the app the way you deploy it, then search the generated client assets (commonly dist for Vite or SvelteKit, .next/static for Next.js, .output/public for Nuxt) for your actual secret values and for telltale shapes like sk- or service_role. 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.
- Suspicious result: the literal value of a real secret appears in a built .js file, or a variable named with a public marker is paired with a value that should never be public.
- Safer result: the only client-exposed values are ones you intended to publish, and no secret-shaped string appears in the client assets.
- The marker on a variable is the cue to check; the value in the built file is the confirmation. Repeat the bundle grep after any rename, because the source intent and the shipped output can disagree.
Fix by defaulting to server-only
The fix principle is one sentence: default every variable to server-only, and promote it to public only when you can state out loud why it is safe in the browser. If you cannot finish the sentence this value is safe for every visitor to read because..., the variable does not get a public marker.
In practice that means a real secret loses its client-exposed marker and moves to server-side code. The client should never hold a privileged key at all. Route the privileged call through a server route, a serverless function, an edge function, or your framework's server layer (a Next.js Route Handler, a Nuxt or SvelteKit server endpoint), which holds the secret in its own runtime and is never inlined into the browser build. The browser calls your endpoint; your endpoint calls the third party. Keep the public marker only on values that are genuinely safe to publish.
- Rename and relocate: a variable holding a secret should lose its public marker and move server-side, not stay in the client under a different name.
- Treat any secret that already shipped in a deployed build as compromised. Rotate it at the provider, then rebuild and re-grep the bundle. Renaming does not un-publish a key that already went out.
- The new server endpoint you create still needs its own authentication and rate limiting. Moving a secret server-side closes the exposure; it does not, by itself, add an auth check to the proxy you just built.
This split governs which env vars reach the browser. It is not your whole secrets story. A key held only in a private server env, or one exposed in an earlier deployment, is outside this model. And the distinction is about exposure, not authorization: a value being server-only does not mean the endpoint that uses it is access-controlled.
Where VibeCodeGuard fits, and what it does not prove
After you fix the split and rebuild, VibeCodeGuard's Launch Check is the external confirmation that the classification actually held. It fetches the JavaScript on your public surface and scans it for strings that match known secret patterns and high-entropy shapes. If a value you meant to keep server-only is still in the rebuilt public bundle, the scan flags it with severity and fix direction, so the leak surfaces in your own check rather than in someone else's traffic. It reads exactly what an outside visitor can download.
What it does not do is read your server-side code, your framework config, or the env vars that never reach the bundle, so it cannot confirm that a value you classified as secret is correctly held on the server. It only confirms the public side. It also cannot tell you whether a flagged key still works; any exposed secret should be rotated regardless.
A clean Launch Check means no known secret-shaped strings were found on the public surface, not that every variable is correctly classified or that no secret ever leaked. Pattern matching can miss an unusual key format, and a key exposed in an earlier deployment is outside its view. Treat a flagged value as a launch blocker and a rotation trigger; treat a clean result as one signal, not a guarantee. Next step: list your variables, classify each as public or secret, confirm only the public ones carry a client-exposed marker, grep the rebuilt bundle, 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.