Skip to content
Back to field notes
FrameworksJune 6, 20266 min read

Nuxt runtime config: what leaks to the client bundle by default

In Nuxt, anything you put under the public runtime config or a NUXT_PUBLIC_ env var ships into the client bundle, so a misplaced secret is readable by any visitor the moment the URL is live.

NuxtEnv varsSecrets

A key under public runtime config ships to every visitor

The concrete risk with nuxt runtime config, public vs private, is that the two halves look almost identical in code but land in completely different places. Anything under runtimeConfig.public is bundled into the JavaScript that the browser downloads. Anything under runtimeConfig (the top-level, non-public side) stays on the server. Put a value on the wrong side and it goes from server-only to world-readable, and nothing in the build fails to warn you.

That is the whole risk in one line: a secret placed under public is not protected by login, by an API gateway, or by anything else. It is a static string sitting in a file your visitors download. A Stripe secret key, an admin API token, a database connection string, an LLM provider key like an sk- prefixed token, a service_role string, all of these are meant to live only on the server. Under runtimeConfig.public, or set through a NUXT_PUBLIC_ environment variable, they ride along into the client bundle.

What is observable is precise: the value is present in the served JavaScript and you can read it in the browser. What is only possible is the next step, that someone finds it and uses it. Treat the first as the fact that matters. If a real secret is in the bundle, the leak has already happened; you do not get to assume nobody looked.

  • runtimeConfig.public.X and the env var NUXT_PUBLIC_X both resolve into the client bundle.
  • runtimeConfig.X (no public) and the env var NUXT_X stay server-side and are not shipped.
  • The danger token is the word public, and the env-var danger token is the NUXT_PUBLIC_ prefix.

Why AI-built Nuxt apps land secrets on the public side

The public side exists for a good reason: the browser legitimately needs some config, like a public site URL, an analytics key meant to be public, or a feature flag. The failure is not that public config exists. It is that the line between public and private is a judgement call, and generated code does not make that judgement reliably.

When you prompt an AI tool to wire up a payment flow or an API integration, the fastest way to make the call succeed is to read the key from config and use it. If the key is referenced in a component or a composable that runs in the browser, the generated code will often reach for runtimeConfig.public, because the private side is not readable client-side and the code would break there. The app then works on the first try, which is exactly what makes the leak invisible. A broken build gets fixed; a working build that quietly publishes a secret looks finished.

  • A generated nuxt.config.ts frequently promotes a value to runtimeConfig.public so a client-side call stops erroring, instead of moving that call to the server.
  • An .env line written as NUXT_PUBLIC_STRIPE_SECRET_KEY will be honored exactly as written and shipped to the browser. The prefix is doing its job; the naming was the mistake.
  • Because the app functions, no error surfaces, and the public URL goes live before anyone audits which values are in the bundle. This is the same fast-deploy gap that exposes source maps; see source maps public exposure in vibe-coded apps.

Check your own bundle before you share the URL

You can confirm this on your own running app with no attack and no special tools. Build and serve the app the way it deploys, then look at what the browser actually receives.

In the browser: open your deployed app, open DevTools, and go to the Sources or Network tab. Search the loaded JavaScript for your known secret prefixes as plain strings, for example sk_live, sk_test, sk-ant-, sk-proj-, service_role, or a fragment of a connection string. You can also open the DevTools console and inspect the runtime config that Nuxt exposes to the client; whatever appears there is, by definition, public. Anything under the public config that is actually a secret is the suspicious result.

In your config and env: read nuxt.config.ts and every .env file. Walk each entry under runtimeConfig.public and every NUXT_PUBLIC_ variable and ask one question per line: is this safe for any visitor to read? A public site URL or a publishable key like a pk_ prefixed Stripe key is fine. A secret key, a token that grants write or admin access, or a database credential is not.

  • Suspicious result: a secret-shaped string appears in the served JavaScript, or a true secret sits under runtimeConfig.public or a NUXT_PUBLIC_ name.
  • Safer result: the public side holds only values you would be comfortable printing on the homepage, and every real secret is under the top-level runtimeConfig with no public and no NUXT_PUBLIC_ prefix.
  • Repeat after each deploy. A clean bundle today says nothing about the build you ship next week, because one renamed env var can move a value across the line.

Move the secret server-side, and know what that does not fix

The fix is to move the value, not to hide it. Take the secret out of runtimeConfig.public and out of any NUXT_PUBLIC_ env var, and place it under the top-level runtimeConfig (set via a plain NUXT_ prefix or a non-prefixed env var, depending on your setup). Then move the code that uses it so it runs on the server, inside a Nuxt server route or server-side composable, never in a component that renders in the browser. The browser calls your server route; your server route holds the key and talks to the third party. The secret never leaves the server.

  • Relocate every true secret to the private runtimeConfig and confirm the client-side code no longer references it directly.
  • Put the privileged call behind a server route under the server directory, and add an authentication and authorization check there. Moving a key server-side stops the key from leaking; it does not stop an anonymous caller from hitting an open endpoint that uses the key. Those are separate gaps; see Nuxt server routes API security for locking down those paths.
  • After moving a value, rebuild and re-check the bundle. The edit is only done when the secret is gone from the served JavaScript.

Moving a secret server-side fixes confidentiality of the key, not authorization on the endpoint that uses it. A server route that calls a paid API with your now-private key is still abusable if anyone can call that route without logging in. Authentication proves who the caller is; authorization decides what they may do. Both still need to be added explicitly, and a rotated key is required if the old one was ever in a public build.

Where VibeCodeGuard fits, and what a clean scan does not prove

This problem is observable from the outside, which is exactly the surface VibeCodeGuard's Launch Check looks at. The scan reads your public bundle and the client-visible network responses and flags secret-shaped strings that have been promoted to the public side, the kind of value that ends up there when a NUXT_PUBLIC_ prefix or runtimeConfig.public entry is used by mistake. If a key that looks like a real secret is sitting in the JavaScript a visitor downloads, that is a direct, reproducible signal, and the scan reports it with severity and fix direction.

What it does not do is read your nuxt.config.ts, judge your intent, or decide for you which values are genuinely meant to be public. It sees what an outside visitor can reach. Confirming that the right values are private, and that your server routes guard the keys you moved, is work that needs the code and a manual review.

Next step: open your deployed app, search the loaded JavaScript for your secret prefixes, and walk every NUXT_PUBLIC_ entry one line at a time. Then run a focused Launch Check against the public URL before sharing it, so a promoted secret shows up in your scan instead of in someone else's.

A clean Launch Check means no secret-shaped string was found on the public surface at scan time. It does not prove every config value is correctly scoped, that your server routes are authenticated, or that a key removed today was not exposed in an earlier deploy. Treat a flagged secret as a launch blocker, rotate it, and treat a clean result as one signal among several, not a guarantee.

> launch check

Scan the public surface before launch.

Get severity, evidence, and practical fix guidance for the checks VibeCodeGuard can run from the outside.