This page is available in English only.

Obfuscate a Next.js build

@afterpack/next obfuscates the client JavaScript inside next build, on Turbopack or webpack, SSR or static export.

@afterpack/next obfuscates your client JavaScript from inside next build. withAfterpack returns a config carrying compiler.runAfterProductionCompile, Next's own build hook, which Next calls once for either bundler.

Wrapping your config is the whole integration. There is no lifecycle script and no separate command.

Install

$ npm install -D @afterpack/next
// next.config.ts
import type { NextConfig } from "next";
import { withAfterpack } from "@afterpack/next";

const nextConfig: NextConfig = {
  // your existing config
};

export default withAfterpack(nextConfig);

The function form of a Next config works the same way. withAfterpack(async (phase, ctx) => ({ … })) returns a wrapped function.

Build

next build now emits obfuscated output, whichever bundler produced .next/ (Turbopack or webpack). next dev is untouched. Every invocation of next build runs the hook, including a CI job that calls it directly, and there is no lifecycle script for an install to skip. If the hook throws, the build fails with exit code 1.

The hook obfuscates the client-served JavaScript only: .next/static/chunks, plus out/ for a static export. .next/server never reaches a browser and is not obfuscated. The hook runs after compilation and before prerendering and static export, so a static export copies obfuscated chunks into out/.

Next writes the chunks before the hook runs, so your unobfuscated chunks sit on disk for about a second during the build, and anything that can read .next/ in that window can read them. The in-pipeline plugins (Vite, Rollup, webpack, Parcel) hand the obfuscated bundle back to the bundler instead, so nothing unobfuscated is ever written.

A compiler.runAfterProductionCompile hook you already set is composed rather than replaced. Yours runs first, AfterPack's second, so yours sees the bytes it would see without AfterPack installed.

Options passed to withAfterpack are closed over by the hook. They outrank the AFTERPACK_* environment variables, which outrank afterpack.json. Use paths.exclude to leave a file untouched.

The combined protectionMap.html is written to the gitignored .afterpack/ at your project root.

Verify

Every protected build writes a receipt to .next/.afterpack-protection.json: the engine version, the seed, the bundler, the build id, and a sha256 per obfuscated file. Assert it in your deploy step:

$ npx afterpack@latest verify .

It exits non-zero when the receipt is missing, when it belongs to a different build, or when any recorded file no longer hashes to the value it was obfuscated to.

Because next build sets NODE_ENV=production, the production-safe posture applies out of the box, and the Protection Map is off by default. Opt back in explicitly to see the full artifacts locally:

export default withAfterpack(nextConfig, { protectionMap: { enabled: true } });

The report lands in the gitignored .afterpack/protectionMap.html. It contains your full source. Never commit it or serve it.

Never ship source maps to production

A .map file contains your original source. Set productionBrowserSourceMaps: true only when you want the Protection Map to show original source. The hook strips the maps from the served tree and keeps the //# sourceMappingURL comment off in production.

Options

Every configuration key can be passed as the second argument to withAfterpack, for example withAfterpack(nextConfig, { preset: "hard", complexity: 40 }).

Directives

Directives are read from your source by the plugin, and need productionBrowserSourceMaps: true to be located in the built chunks. See Directives.

Edge cases

  • experimental.sri is refused. Next computes each asset's integrity hash while it writes the asset, before any build hook can run, so a chunk rewritten afterwards is blocked by the browser. Remove experimental.sri, or remove withAfterpack.
  • Point at .next/, not dist/. If you use the generic CLI instead of this package, npx afterpack@latest .next/static/chunks is the equivalent target, but it does not strip served maps for you.
  • Fail-closed on unobfuscated files, with no override. If the engine cannot safely obfuscate a file, the build fails and names the offender. Exclude the file from the pass entirely with paths.exclude if it should never be obfuscated.

Next