Packaging
Turning an Electron app into signed, notarized installers — electron-builder vs Forge, asar, per-platform targets, code signing, notarization, and architectures
Packaging
Packaging is the step that turns a folder of JavaScript into a .dmg, .exe, or .AppImage a user can install — correctly signed so the OS trusts it. Get this wrong and users see scary warnings, downloads get quarantined, or auto-update silently refuses to apply.
Choosing a Tool
Three tools dominate. They differ in configuration style and how much of the signing/updating story they own.
| Tool | Config style | Signing | Updater | Plugins | Notes |
|---|---|---|---|---|---|
| electron-builder | Declarative YAML/JSON build block | macOS + Windows + notarization built in | First-class via electron-updater | Limited (hooks) | The common default; batteries included |
| Electron Forge | JS config + makers/plugins | Via makers (@electron-forge/maker-*) | Squirrel-based | Rich plugin model (Vite/Webpack) | The official toolchain; pick for the plugin ecosystem |
| @electron/packager | Programmatic API only | You wire signing yourself | None | None | Low-level; produces an app bundle, not installers |
@electron/packager only produces the unpacked .app/exe directory — it does not make installers or handle the update feed. Most teams want one of the higher-level tools.
Pick electron-builder unless you have a reason not to. It covers macOS signing + notarization, Windows Authenticode, all the common installer targets, and integrates directly with electron-updater (see the Auto-Update page). Choose Forge when you want the official, plugin-driven toolchain (its Vite/Webpack plugins and maker model) and are comfortable wiring the updater yourself.
The asar Archive
asar is a simple concatenated archive (like an uncompressed tar) of your app source. Electron reads files directly out of it, so most fs reads "just work" against paths inside the archive. It exists to avoid shipping thousands of small files (faster install, fewer path-length issues on Windows) and to lightly obscure source — it is not encryption.
Some files cannot live inside asar and must be unpacked to disk:
- Native
.nodemodules — the OS loader needs a real file path todlopen. - Sidecar binaries / executables you spawn with
child_process. - Anything addressed by an absolute real path (some libraries that
forka worker file).
# electron-builder.yml
asar: true
asarUnpack:
- "**/*.node"
- "node_modules/sharp/**"
- "resources/ffmpeg*"Unpacked files land in app.asar.unpacked/ next to app.asar. At runtime resolve them by replacing app.asar with app.asar.unpacked in the path (electron-builder’s tooling and many libraries do this for you).
asar is not a security boundary. Anyone can run npx @electron/asar extract app.asar out/ and read your code. Do not ship secrets (API keys, signing material) inside the bundle — they belong on a server or in OS-protected credential storage.
Per-Platform Targets
Each OS has several installer formats. Pick the ones your audience expects.
| Platform | Target | Use it for |
|---|---|---|
| macOS | dmg | Standard drag-to-Applications installer |
| macOS | zip | Required for Squirrel.Mac auto-update feed |
| macOS | mas | Mac App Store (sandboxed, separate provisioning) |
| Windows | nsis | Default installer; supports per-user/per-machine, differential updates |
| Windows | msi | Enterprise/MDM deployment |
| Windows | portable | Single self-contained exe, no install |
| Windows | appx | Microsoft Store |
| Linux | AppImage | Distro-agnostic single file; supports auto-update |
| Linux | deb / rpm | Debian/Ubuntu and Fedora/RHEL system packages |
| Linux | snap / flatpak | Sandboxed store distribution with self-update |
A typical config ships dmg+zip on macOS (zip feeds the updater), nsis on Windows, and AppImage+deb on Linux.
Code Signing
Signing proves the binary came from you and was not tampered with. Unsigned apps trigger Gatekeeper (macOS) and SmartScreen (Windows) warnings, and unsigned macOS builds cannot auto-update at all.
macOS — Developer ID
For distribution outside the App Store you sign with a Developer ID Application certificate, enable the hardened runtime, and grant any required entitlements.
<!-- build/entitlements.mac.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key><true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key><true/>
<!-- only if you use the camera / mic -->
<key>com.apple.security.device.camera</key><true/>
</dict>
</plist>electron-builder picks up the certificate from the login keychain (or CSC_LINK + CSC_KEY_PASSWORD in CI) and signs every nested binary, including unpacked native modules.
Windows — Authenticode
Windows binaries are signed with an Authenticode code-signing certificate.
| Certificate | SmartScreen behavior |
|---|---|
| OV (Organization Validation) | Builds reputation over time; new versions may still warn until enough installs accrue |
| EV (Extended Validation) | Instant SmartScreen trust; no warning from day one |
EV (and increasingly OV) certificates now require the private key on a hardware token (FIPS HSM) or a cloud signing service (Azure Trusted Signing, DigiCert KeyLocker, SSL.com eSigner). You cannot export the key to a file, so CI signs by calling the cloud/HSM provider rather than reading a .pfx.
Why signing matters. On macOS, Gatekeeper refuses to launch an unsigned/un-notarized app downloaded from the internet ("…is damaged and can’t be opened"). On Windows, SmartScreen shows a red "unrecognized app" wall. Either one tanks your install conversion. Treat signing as a release requirement, not a nice-to-have.
macOS Notarization
Signing is necessary but not sufficient: macOS also requires notarization. You upload the signed app to Apple, their service scans it for malware, and returns a ticket. You then staple that ticket to the artifact so Gatekeeper can verify it offline.
The modern flow uses notarytool (the legacy altool is removed). electron-builder runs it automatically when notarization credentials are present:
# electron-builder.yml
mac:
hardenedRuntime: true
gatekeeperAssess: false
entitlements: build/entitlements.mac.plist
entitlementsInherit: build/entitlements.mac.plist
notarize: true # uses APPLE_ID / APPLE_APP_SPECIFIC_PASSWORD / APPLE_TEAM_ID
# or APPLE_API_KEY / APPLE_API_KEY_ID / APPLE_API_ISSUER# what runs under the hood (managed for you by electron-builder):
xcrun notarytool submit App.dmg --apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAM_ID" --password "$APPLE_APP_SPECIFIC_PASSWORD" --wait
xcrun stapler staple App.dmg # attach the ticket for offline verificationNotarization is required for any app distributed outside the Mac App Store on current macOS. Without it, Gatekeeper blocks the app on first launch.
Architectures
Apple Silicon and Intel are different CPU architectures, as are Windows/Linux x64 vs arm64.
| Architecture | Where |
|---|---|
x64 | Intel/AMD macOS, Windows, Linux |
arm64 | Apple Silicon (M-series), Windows on ARM, ARM Linux |
universal | macOS fat binary containing both x64 and arm64 |
A macOS universal build runs natively everywhere but roughly doubles download size. Alternatively ship separate x64 and arm64 artifacts and let the update feed serve the right one. On Windows/Linux, ship arm64 only if you have demand — most users are still x64.
electron-builder --mac --x64 --arm64 # two separate mac artifacts
electron-builder --mac --universal # one fat binaryA Representative Config
A complete electron-builder.yml for a multi-platform, signed, auto-updating app:
# electron-builder.yml
appId: com.example.myapp
productName: My App
copyright: Copyright © 2026 Example Inc.
directories:
output: dist
buildResources: build
# Ship only what the runtime needs — never source or devDependencies.
files:
- "out/**/*" # compiled main/preload/renderer
- "package.json"
- "!**/*.{ts,map,md}" # prune source maps, types, docs
- "!**/{test,__tests__,.github}/**"
asar: true
asarUnpack:
- "**/*.node"
mac:
category: public.app-category.productivity
target:
- { target: dmg, arch: [x64, arm64] }
- { target: zip, arch: [x64, arm64] } # zip feeds the auto-updater
hardenedRuntime: true
entitlements: build/entitlements.mac.plist
entitlementsInherit: build/entitlements.mac.plist
notarize: true
win:
target:
- { target: nsis, arch: [x64] }
# signing handled via cloud HSM / signtool config or CSC_* env vars
nsis:
oneClick: false
perMachine: false
allowToChangeInstallationDirectory: true
linux:
target: [AppImage, deb]
category: Utility
publish:
provider: github # generates latest.yml / latest-mac.yml for electron-updater
owner: example
repo: myappThe publish block is what makes auto-update possible: electron-builder writes the metadata files (latest.yml, latest-mac.yml, *.blockmap) the updater reads. See the Auto-Update page.
What Ships vs What Doesn’t
Your installer must contain the runtime app and its production dependencies — and nothing else. Two common approaches:
filespruning (above) — keep onepackage.json, but usefilesglobs to exclude source, tests, and dev tooling. electron-builder already stripsdevDependenciesfromnode_moduleswhen it rebuilds the production tree.- Two-package.json structure — a root
package.jsonfor build tooling and anapp/package.jsonwith only runtime deps. Theapp/folder is what gets packaged. This makes the boundary explicit and is the Forge-friendly layout.
Native deps must be real dependencies, not devDependencies. Anything required at runtime — better-sqlite3, sharp, your own native addon — has to be a production dependency or it will be pruned out of the package and the app will crash on launch with "module not found". Build-only tools (electron, electron-builder, bundlers) stay in devDependencies.
Reproducibility and Secrets
- Pin versions. Pin
electron,electron-builder, and native deps exactly, and commit a lockfile. An Electron bump changes the ABI and can break native modules; an unpinned builder can change output formats. - Pin the Electron version explicitly — auto-update artifacts must be built against a known runtime, and a surprise major bump can break signing or notarization.
- Keep signing secrets in CI, never in the repo. Certificates,
APPLE_API_KEY, Authenticode tokens, and provider credentials live in CI secret storage and are injected as environment variables at build time. A leaked Developer ID or code-signing key lets anyone ship malware as you.
Checklist
- Choose electron-builder (or Forge with the updater wired) — not raw packager for distribution.
asar: true;asarUnpacknative.nodemodules and sidecar binaries.- Build the targets your users expect; include macOS
zipfor the updater. - macOS: Developer ID + hardened runtime + entitlements + notarize.
- Windows: Authenticode (EV to skip SmartScreen); sign via HSM/cloud in CI.
- Ship
arm64where it matters; universal mac if you want one artifact. - Runtime/native deps in
dependencies; prune source and devDeps from the package. - Pin versions, commit the lockfile, keep all signing secrets in CI.