# Obfuscate an Electron app

@afterpack/electron wires main, preload and renderer with one seed in a single call, and refuses electron-vite's bytecodePlugin and unguarded backups before they can ship your source.

Source: https://www.afterpack.dev/docs/frameworks/electron

`@afterpack/electron` obfuscates all three programs one Electron app ships (main, preload, renderer) from a single call, sharing one build seed across them. Wired by hand, each leg is its own bundler build and draws its own random seed, so the parts of one application come out structurally unrelated to each other.

electron-builder is not a bundler. It packages an already-built app into an installer and never touches your JavaScript, so it needs a bundler above it regardless of AfterPack.

## Install

```bash
npm install -D @afterpack/electron
```

```ts
// electron.vite.config.ts
import { withAfterpack } from "@afterpack/electron";
import { defineConfig, externalizeDepsPlugin } from "electron-vite";

export default withAfterpack(
  defineConfig({
    main: { plugins: [externalizeDepsPlugin()] },
    preload: { plugins: [externalizeDepsPlugin()] },
    renderer: {},
  }),
);
```

`withAfterpack` wraps an entire electron-vite config (the object, function or promise form `defineConfig` returns) and wires [`@afterpack/vite`](https://www.afterpack.dev/docs/frameworks/vite) into every leg the config declares, never one it doesn't. Peer dependency: `vite ^5 \| ^6 \| ^7 \| ^8`.

## Build

How you wire the pass depends on whether your toolchain configures all three legs in one call, or one config per leg.

### One leg at a time

Toolchains that configure each leg separately (Electron Forge with `@electron-forge/plugin-vite`) call `afterpackElectron` once per config instead:

```ts
// vite.main.config.ts   (and vite.preload.config.ts / vite.renderer.config.ts)
import { afterpackElectron } from "@afterpack/electron";

export default { plugins: [afterpackElectron({ leg: "main" })] };
```

`leg` is required.

### One seed, three ways

- **electron-vite (`withAfterpack`)** and **Forge + `plugin-vite`**: all three configs build in one Node process, so the legs share a seed automatically.
- **`vite-plugin-electron`**: also one process. Place `afterpackElectron({ leg: "renderer" })` last in `plugins`, after `vite-plugin-electron` itself, so AfterPack's `generateBundle` seals the finished bundle.
- **Hand-rolled scripts that run each leg as a separate command**: there is no shared process, so pin the seed across them with an environment variable:

```jsonc
// package.json
"build": "AFTERPACK_seed=git vite build -c vite.main.ts && vite build -c vite.renderer.ts"
```

`AFTERPACK_seed` takes the same values as [`seed`](https://www.afterpack.dev/docs/config#seed): an integer, `"git"` (derived from `HEAD`), or any other string, hashed by the engine. An explicit `seed` option still wins over it. [Seed strategy](https://www.afterpack.dev/docs/best-practices#seed-strategy) covers when to pin one and when not to.

## Verify

The [Protection Map](https://www.afterpack.dev/docs/protection-map) is written per leg, into `.afterpack/<leg>/protectionMap.html`, when a source map was discoverable and the build isn't running under [`NODE_ENV=production` / `CI=true`](https://www.afterpack.dev/docs/config#build-mode). Inspect main, preload and renderer separately. Each report contains your full source. Never commit it or serve it.

> **Never ship source maps inside the app**
>
> A `.map` in `app.asar` reaches your original source. Keep source-map emission off (the default in production) for anything you package.

## Options

Every [configuration key](https://www.afterpack.dev/docs/config) works here too, applied to every leg through `withAfterpack(config, options)` or forwarded per leg through `afterpackElectron(options)`. These options exist only on this package:

| Option | Type | Default |
|---|---|---|
| `leg` | `"main" \| "preload" \| "renderer"` | required on `afterpackElectron` |
| `projectRoot` | `string` | `process.cwd()`. electron-vite roots the renderer build at `src/renderer/`. Override it if your build runs from somewhere else |

[`seed`](https://www.afterpack.dev/docs/config#seed) defaults to one fresh value shared by every leg in a build session. [`protectionMap.enabled`](https://www.afterpack.dev/docs/config#protectionMap-enabled) follows the usual source-map rule, and each leg writes its own report to `.afterpack/<leg>/protectionMap.html`.

```ts
// Heavier protection, reproducible per commit, applied to every leg:
withAfterpack(config, { preset: "hard", seed: "git" });
```

## Directives

Each leg wires [`@afterpack/vite`](https://www.afterpack.dev/docs/frameworks/vite) underneath, so [directive capture](https://www.afterpack.dev/docs/frameworks/vite#directives) works the same way there.

## Edge cases

### Support matrix

| Toolchain | Wiring | One seed automatically? |
|---|---|---|
| electron-vite | `withAfterpack(defineConfig({ … }))`, one call | Yes |
| Forge + `plugin-vite` | `afterpackElectron({ leg })` in each of the three configs | Yes |
| Forge + `plugin-webpack` | [`@afterpack/webpack`](https://www.afterpack.dev/docs/frameworks/webpack) in each config | Yes |
| `vite-plugin-electron` | `afterpackElectron({ leg: "renderer" })`, placed **last** | Yes, once placed after that plugin |
| electron-builder | it packages, it doesn't bundle; wire the bundler above it | n/a |
| Hand-rolled scripts (separate processes) | a plugin per leg, or `npx afterpack@latest <dir>` | No; pin `AFTERPACK_seed` |

### What it refuses

Two fail-closed checks apply on any leg wired through this package.

**`bytecodePlugin`.** electron-vite's own `bytecodePlugin` replaces every emitted chunk with a 3-line loader stub and ships the real application as V8 bytecode (`.jsc`). The two cannot compose, because bytecode replaces the chunk Rollup generated. The build fails on the leg where `bytecodePlugin` is enabled, or wherever a `.jsc`/`.cjsc` file turns up in the output afterward. Drop one or the other.

**`build.backup: true`.** [Backups](https://www.afterpack.dev/docs/config#build-backup) are refused outright: the call throws instead of proceeding, because a `.backup.<hash>.js` is your complete original source, written next to the output inside the tree your packager copies into `app.asar`. Every user who installs the app would receive it.

The same risk extends to any artifact your packager doesn't know to exclude. electron-builder's default `files` glob matches with `dot: true` and does not exclude `.afterpack/` by default. Exclude these from what you package:

```jsonc
// electron-builder
"files": ["**/*", "!.afterpack/**", "!**/*.backup.*", "!**/*.map"]
// electron-forge
packagerConfig: { ignore: [/^\/\.afterpack/, /\.backup\./, /\.map$/] }
```

[Source maps](https://www.afterpack.dev/docs/config#sourceMap-enabled) get a warning rather than a refusal: a `.map` with `sourcesContent` inside `app.asar` is full deobfuscation. Keep `sourceMap: { enabled: true, emitUrl: true }` to local debug builds and exclude `*.map` from what you package.

### IPC channels

Channel names must match byte-for-byte across the two bundles. `ipcMain.on("save-file", …)` and `ipcRenderer.send("save-file", …)` are the same string on both sides. They're ordinary string literals in ordinary emitted JavaScript, so keeping both legs on the same [seed](https://www.afterpack.dev/docs/config#seed) and the same posture ([`preset`](https://www.afterpack.dev/docs/config#preset) / [`complexity`](https://www.afterpack.dev/docs/config#complexity)) is what keeps the two halves consistent.

If a channel name is assembled at runtime rather than written as a literal, keep that construction in a module you exclude with [`paths.exclude`](https://www.afterpack.dev/docs/config#paths-exclude), or lower the posture for that region through a bundler-plugin [`regions`](https://www.afterpack.dev/docs/config#regions) entry ([Pro](https://www.afterpack.dev/docs/pro#what-pro-buys)).

### Without the plugin: the CLI

Prefer not to add a dependency, or building a leg with something other than Vite? Run [`npx afterpack@latest`](https://www.afterpack.dev/docs/cli) once per output directory after the build instead:

```bash
npx afterpack@latest out/main --seed=git
npx afterpack@latest out/renderer --seed=git
```

The [path argument is required](https://www.afterpack.dev/docs/cli#usage); there's no auto-detection. Each run walks the directory, collects every `.js`/`.mjs`/`.cjs`, and obfuscates them [in place](https://www.afterpack.dev/docs/cli#what-a-run-does); a single file works as the argument too. Pin the same [`--seed`](https://www.afterpack.dev/docs/config#seed) across every run: `git`, derived from the current commit, is the simplest way to guarantee it. A CLI run gets none of the refusals above; it obfuscates whatever JavaScript it finds. Keep the same [`--seed`](https://www.afterpack.dev/docs/config#seed) and [`--preset`](https://www.afterpack.dev/docs/config#preset) across runs.

## Next

- [Frameworks](https://www.afterpack.dev/docs/frameworks): the full matrix.
- [Vite](https://www.afterpack.dev/docs/frameworks/vite): the option semantics this package forwards.
- [CLI reference](https://www.afterpack.dev/docs/cli#flags): the fallback path's flag list.
- [Builds & CI](https://www.afterpack.dev/docs/builds): seeds, dev-vs-prod, promoting the same bytes.
- [Protect an Electron app's license logic](https://www.afterpack.dev/docs/use-cases/electron-license-logic): applying this to a local license or trial check.
- [Best practices → Seed strategy](https://www.afterpack.dev/docs/best-practices#seed-strategy): when to pin, when not to.
- [Protection Map](https://www.afterpack.dev/docs/protection-map): reading the report.
- [Configuration](https://www.afterpack.dev/docs/config): every option, and which surface can set it.
