# Obfuscate your build in CI

Where AfterPack sits in your pipeline: production only, build once and promote everywhere, with a CI recipe and the rules for seeds, artifacts, source maps, and a down cloud.

Source: https://www.afterpack.dev/docs/builds

AfterPack is a post-build step: your bundler produces JavaScript, then AfterPack obfuscates it. We recommend obfuscating once, in the production build, and promoting that artifact unchanged through every stage. One line below is not a recommendation: a deployed source map hands back your original source, so keep maps out of the deploy.

## Dev is not obfuscated

Obfuscation runs on built output, so there is nothing for it to do against a dev server. Vite dev, `next dev`, and HMR serve unbuilt or partially built code.

- **Development.** Your bundler runs as usual. AfterPack is idle. Stack traces point at your real source, HMR is instant, debugging is normal.
- **Production build.** AfterPack runs after the bundle closes. A [framework plugin](https://www.afterpack.dev/docs/frameworks) runs it automatically. On a pure-bundler stack you invoke it once.

To build production without obfuscation, for example a one-off debug build, set [`{ build: { autorun: false } }`](https://www.afterpack.dev/docs/config#build-autorun) in your config or [`AFTERPACK_build_autorun=false`](https://www.afterpack.dev/docs/config#build-autorun) in the environment.

## Build once, promote the artifact

The obfuscated bundle is the artifact you ship, so the cleanest pipeline builds it once and moves the same bytes through every stage.

*(build-once, promote-everywhere flow diagram — see the web page)*

Re-running AfterPack per environment is the alternative, and it costs you your test signal: each hop produces a differently shaped bundle, so what QA exercised is not what production serves. Promoting one artifact keeps the integration tests you run in QA running against the exact bytes that reach production. Your dev loop stays un-obfuscated either way.

## The command surface

```bash
npx afterpack@latest dist/ --preset=hard
```

A path to the emitted JavaScript, plus [every config key as a flag](https://www.afterpack.dev/docs/cli#flags). Naming the build directory explicitly is worth it in CI, where an auto-detected one is a silent way to obfuscate the wrong tree. `npx afterpack@latest verify [dir]` re-checks a build against the [protection receipt](https://www.afterpack.dev/docs/frameworks/nextjs#verify) it wrote, which is the step to run before you deploy. The [CLI reference](https://www.afterpack.dev/docs/cli#usage) has the full surface.

## Random seed by default, pin for reproducibility

[The seed](https://www.afterpack.dev/docs/config#seed) makes each release structurally different from the last, so reversing last week's bundle buys nothing against this one. A fresh random seed per release is the default. You don't set anything to get it.

The seed is not a secret. Store it like any other build parameter, in a CI variable.

Pin it only when you need reproducible output: build-cache hits, golden-snapshot tests, or auditable byte-for-byte rebuilds.

```bash
npx afterpack@latest dist/ --seed=git
```

[`--seed=git`](https://www.afterpack.dev/docs/config#seed) resolves to `git rev-parse HEAD`. Outside a repository it falls back to a fresh random seed and prints a notice rather than failing the build. Any other string is hashed by the engine; an integer is used verbatim. A pinned seed produces identical output every time.

> **Pin to something that still changes per release**
>
> A seed that is constant across releases carries an attacker's analysis from one to the next, which is the property you installed AfterPack for. When a pinned seed earns its place, pin it to a value that moves every release, such as the commit SHA.

## Rotate on a schedule

Random-per-release seeding already varies the artifact each time you ship. Teams that ship rarely often add a scheduled rebuild on top, a nightly or weekly run from the same source, so the deployed bundle changes shape every cycle even when the code doesn't.

An attacker who started reversing build N is reset on build N+1: there is no shared structure between the two builds to carry the work over. This matters most for anti-bot, anti-cheat, and CAPTCHA-resistant logic, where the adversary has time. Rotation does not by itself stop cheating or bot traffic; client-side logic still needs server-authoritative validation behind it.

## In CI

The pipeline is build, then obfuscate, then keep the artifacts out of the deploy. With a framework plugin, the obfuscate step is part of your normal `build` and you can drop the explicit AfterPack call. On a pure-bundler stack, run it yourself.

```yaml
# .github/workflows/release.yml (excerpt)
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # 1. Build. With @afterpack/<fw> installed, obfuscation runs here automatically.
      - run: npm ci
      - run: npm run build

      # 2. Pure-bundler stacks only (no framework plugin): obfuscate explicitly.
      #    Pin --seed for Turborepo/Nx remote-cache hits; omit it for a fresh
      #    random seed per release. --diagnostics.level=all prints every diagnostic
      #    instead of rolling the info ones up to one line.
      - run: npx afterpack@latest dist/ --preset=hard --seed=git --diagnostics.level=all

      # 3. Keep the Protection Map as a reviewable CI artifact. It is written to
      #    the gitignored .afterpack/ at the PROJECT ROOT, never into the deploy
      #    tree. It embeds your ORIGINAL SOURCE — treat the upload as
      #    source-confidential.
      - uses: actions/upload-artifact@v4
        with:
          name: protection-map
          path: .afterpack/

      # 4. Deploy only the bundle. Never the .afterpack/ directory, never a .map.
      - run: ./deploy.sh dist/
```

Turborepo and Nx key their remote cache on output hashes. A random seed produces a different bundle every run, so those caches always miss. Pass [`--seed=git`](https://www.afterpack.dev/docs/config#seed) for cache hits: the SHA still changes per release, so builds stay reproducible within a commit while changing across releases.

Attach the Protection Map to the PR so a reviewer can confirm which regions changed protection level. It is the fastest way to catch a refactor that accidentally moved a secret out of a marked region. Treat it as a reviewer artifact.

> **There is no `afterpack audit ./dist`**
>
> The audit that powers the [security scanner](https://www.afterpack.dev/security-scanner) runs against a URL and prints text; it does not take a local directory. Wire it against your deployed staging URL after the deploy step. See [Audit](https://www.afterpack.dev/docs/audit).

## Artifacts, and keeping them out of the deploy

AfterPack writes three kinds of local artifact. Each is more sensitive than the bundle it describes, and all three default to the safe side.

| Artifact | Default | Where it goes | Flag |
| --- | --- | --- | --- |
| [**Protection Map**](https://www.afterpack.dev/docs/protection-map) (`protectionMap.html`) | [On](https://www.afterpack.dev/docs/config#protectionMap-enabled) when the bundler emitted a discoverable source map, opt-in otherwise | Always the gitignored [`.afterpack/`](https://www.afterpack.dev/docs/cli#files) directory at the project root, never beside the served build | [`--protectionMap.enabled=false`](https://www.afterpack.dev/docs/config#protectionMap-enabled) |
| **Backup of your original files** | [On](https://www.afterpack.dev/docs/config#build-backup) for the CLI, off for a framework plugin | `.afterpack/backup/` at the project root for the CLI; a `.backup.<hash>` sibling beside the output when a plugin is asked for one | [`--build.backup=false`](https://www.afterpack.dev/docs/config#build-backup) to opt out |
| **`.map` source-map sibling** | [Off in production](https://www.afterpack.dev/docs/config#sourceMap-enabled), emitted in dev only if an input map was discovered | Beside the emitted JS | [`--sourceMap.enabled=false`](https://www.afterpack.dev/docs/config#sourceMap-enabled) |

Two things happen automatically, on every integration:

- **`.gitignore` is guarded.** The project root's `.gitignore` gains `.afterpack/`, `*.protectionMap.html`, `protectionMap.html`, `*.backup.*` and `*.map` under a managed header. Only missing entries are appended.
- **A served-path artifact warns.** AfterPack warns if an artifact lands in a served directory. A served source map is full deobfuscation.

The backup is your original source verbatim. The CLI keeps it at the project root, under the gitignored `.afterpack/backup/`, with a manifest recording what each file was obfuscated to; `npx afterpack@latest restore` puts the originals back over a run. A plugin asked for `build.backup` writes its copy beside the output instead, where the deploy would pick it up.

> **A leaked source map undoes the obfuscation**
>
> Source maps chain obfuscated output straight back to your original source, with [`sourcesContent`](https://www.afterpack.dev/docs/config#sourceMap-sourcesContent) carrying the source itself. Never deploy one.

[`@afterpack/next`](https://www.afterpack.dev/docs/frameworks/nextjs) deletes every `.js.map` from the served tree after the obfuscation pass reads them (`.next/static/chunks`, `out/`) and strips the trailing `//# sourceMappingURL=` comments. On other stacks, keep your bundler's source-map output out of the deployed directory.

> **A file the engine can't obfuscate fails the build**
>
> The build stops and names the file. Use [`paths.exclude`](https://www.afterpack.dev/docs/config#paths-exclude) to carve out one that must ship untouched; [Diagnostics](https://www.afterpack.dev/docs/diagnostics) has the codes and what each one means.

## The CI gate

The CLI returns [`0` or `1`](https://www.afterpack.dev/docs/diagnostics#exit-codes), nothing else, so the gate is `if ! npx afterpack@latest dist/ ...; then fail; fi` and never a branch on a specific nonzero code.

A `1` covers everything that did not ship the protection you configured: a usage error, an empty build directory, an engine error, or a [Pro](https://www.afterpack.dev/docs/tiers) build with a [`key`](https://www.afterpack.dev/docs/config#key) that could not reach the [AfterPack cloud](https://www.afterpack.dev/docs/deployment-modes). That last case fails the build rather than falling back to weaker output, so a green run means the protection you asked for is what shipped. Free builds are fully local and never call the cloud. [Diagnostics](https://www.afterpack.dev/docs/diagnostics#exit-codes) owns the full contract and every code a build can emit.

## Watching builds after they ship

Every Pro Cloud build lands in the dashboard: when it ran, which commit it came from, how many files it processed, what it cost in [MB](https://www.afterpack.dev/docs/dashboard/usage) and build time, and its coverage and [inflation](https://www.afterpack.dev/docs/config#inflation-max). Use it to track your rotation cadence and compare builds over time. See [Builds in the dashboard](https://www.afterpack.dev/docs/dashboard/builds).

Free local builds never reach AfterPack, so they never appear there.

## Next

- [Best practices](https://www.afterpack.dev/docs/best-practices): what to mark, what to leave alone, and how to keep CI honest.
- [Builds in the dashboard](https://www.afterpack.dev/docs/dashboard/builds): the per-build record of everything this page produced.
- [Threat model](https://www.afterpack.dev/docs/threat-model): why a build that changes shape every release is the primary defense.
- [CLI reference](https://www.afterpack.dev/docs/cli#flags): the full flag table behind the synopsis above.
- [Diagnostics](https://www.afterpack.dev/docs/diagnostics#exit-codes): the exit contract and the fail-closed rule, in full.
- [Protection Map](https://www.afterpack.dev/docs/protection-map): reading the artifact your CI job uploaded.
