Nuxt & Nitro

Half of your Nuxt app is private. The other half is a public download.

Nuxt draws a real line: everything under server/ compiles into the Nitro bundle and never leaves your host, while everything in .output/public/_nuxt is fetched by every visitor and reads back in full after one formatter pass. Most Nuxt protection work is just deciding which side each piece of logic belongs on — and obfuscating what genuinely has to ship.

Reality Check

nuxt build → .output/public/_nuxt/

Hashed filenames and minified syntax. Neither is a security control — the logic inside is unchanged.

Nitro stays server-sideserver/api and private runtimeConfig never ship.
Protect the output.vue files are compiler input; _nuxt chunks are the release.
Payload keys preserveduseState and useAsyncData keys stay reserved.
Threat Model

Universal code is public code

The thing that makes Nuxt pleasant to write is also the thing that catches teams out: a composable you author once runs on both the server and the client, which means it ships. Unless a file is under server/, named .server.vue, or otherwise excluded from the client build, assume a stranger can read it — because they can, in about thirty seconds.

Composables describe your domain

A useSubscription() or useEntitlements() composable in clear text hands over your plan names, your limits, your workflow states, and the exact shape of the objects your API returns.

The payload is plain text

Server state is serialised into the rendered HTML so the client can hydrate without refetching. Whatever you put there is readable in View Source — obfuscating the code that consumes it changes nothing.

Gates read as instructions

A visible if (user.plan === 'pro') tells an attacker which value to fake and which branch to flip. Protected control flow turns a glance into a project.

Protect after Nuxt builds
npx nuxi build                  # -> .output/
npx jso-protector \
  --preset maximum \
  --input .output/public \
  --output .output/public-protected
# swap the protected folder in before you deploy
The Order of Operations

Build → protect → deploy

  • Never obfuscate .vue, pages/, or composables/. Nuxt resolves routes, layouts, and auto-imports from real file and directory names at build time.
  • Protect the client output in one pass so identifiers agree across the entry chunk and every lazily loaded route chunk — see obfuscation and code splitting.
  • Leave hashed filenames alone. The build manifest maps route to chunk filename; protect file contents, not names, and keep the folder structure identical.
  • Reserve the runtime string contracts — route paths, dynamic params, named middleware, useState/useAsyncData keys, and any key read off an API response.
  • Smoke test the protected build — a cold SSR load, a client-side navigation to a lazy route, a form submit, a page refresh that rehydrates state, and one route behind middleware.
The Nitro Advantage

Spend the server boundary before you spend on transforms

Nuxt gives you something a plain SPA does not: a build-enforced split. Route handlers in server/api, server-only utilities, and the private half of runtimeConfig are compiled into the Nitro bundle and are simply absent from what the browser downloads. That is stronger than any transform, and it costs nothing. Obfuscation is for the remainder — and there is always a remainder.

Move the decision, not just the code

Entitlement checks, pricing maths, and anything touching a credential belong in a Nitro route. What ships to the browser should render an answer it was given, not compute one it could be tricked into.

Know what runtimeConfig exposes

Keys under public are embedded in the client payload deliberately. They are configuration, never secrets. The top-level block stays with Nitro — that is where a real key goes.

Then protect what must ship

Interactive logic, offline behaviour, proprietary client-side algorithms, licence gating. That remainder is exactly what JavaScript obfuscation is for.

What To Exclude

Names Nuxt matches as strings at runtime

Renaming is safe wherever the compiler already resolved a reference. It is unsafe wherever a literal string is matched against a name while the app runs. That single distinction is most of your configuration — the mechanics are in the variable exclusion list.

State and fetch keys

useState('cart') and useAsyncData('invoices', ...) address entries in the hydration payload by string. The server writes under one key and the client reads under the same one; rename either side and you get a silent refetch or an undefined.

Route paths and params

Nuxt builds its router from the filesystem, and the generated manifest matches path strings against the URL. Those literals, and the param names you read off route.params, are contracts with the browser.

Named middleware

definePageMeta({ middleware: 'auth' }) is a string lookup into a registry keyed by filename. If the name is renamed on one side only, the guard stops running — and a guard that stops running fails open.

Layered Defense

Three layers doing three different jobs

Minification — the size layer

Vite already does it during nuxt build. Smaller download, fully reversible with a formatter, not a security control — see minification vs obfuscation.

Obfuscation — the comprehension layer

An encrypted string pool, flattened control flow, a per-build polymorphic decoder, and VM bytecode for the handful of functions that matter most. This is the layer that survives beautifying.

Runtime defense — the behavior layer

Runtime defense notices tampering and debugger attachment, so a modified bundle reports in rather than quietly hammering your Nitro routes.

Frequently Asked

Nuxt protection questions, answered

Does SSR hide my code?

No. SSR decides where the first render happens. The client bundle still downloads so the page can hydrate and stay interactive, and that bundle is your application.

What about nuxt generate?

A fully static build has no Nitro server at runtime, so every line you wrote is in the output. Static sites need more care about what logic ships, not less.

Do Nuxt modules survive?

Module code is resolved and inlined at build time, so by protection time it is ordinary JavaScript in your chunks. Reserve any name a module resolves by string at runtime.

Will it break Pinia stores?

Internal state is fine. The store id is not — it keys the serialised payload, exactly like useState. Reserve store ids and any key persisted to storage between releases.

Will it hurt performance?

Renaming and string protection are effectively free. VM protection is meaningfully slower than native JavaScript, so scope it to licensing and entitlement paths — never a reactive render path.

How do I read production stack traces?

Keep each build’s identifier map private and use symbolication to translate traces back to real names. Do not publish the map beside the bundle.

Start Now

Read your own bundle the way an attacker would

Open your deployed Nuxt app, pull the largest file from /_nuxt/ in the network panel, and run it through a formatter. Everything you can read there, everyone else can too. Then paste the same file into the online obfuscator and compare the two.

Related Guides

Protecting other JavaScript targets

Same protection engine, different build pipeline. These guides cover the platforms closest to this one:

Vue apps · Next.js apps · Svelte apps · TypeScript projects · Obfuscation glossary · Protecting JavaScript (overview)