Technical Documentation: electron-userland/electron-builder
ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference
Overview (`README.md`)
electron-builder [](https://www.npmjs.com/package/electron-builder) [](https://yarn.pm/electron-builder)
A complete solution to package and build a ready for distribution Electron app for macOS, Windows and Linux with “auto update” support out of the box. 📦
Always looking for community contributions! 👀 Setting up a dev environment is easy to do 🪩
Sponsors
| Notes, Tasks, Projects.All in a Single Place. | |
|---|---|
| Your gateway to understanding your diabetes data | An open, source-available software licensing and distribution API |
| ToDesktop: An all-in-one platform for building and releasing Electron apps | Dashcam: Capture the steps to reproduce any bug with video crash reports for Electron. |
Documentation
See the full documentation on electron.build.
- NPM packages management:
- Native application dependencies compilation (including Yarn support).
- Development dependencies are never included. You don't need to ignore them explicitly.
- Two package.json structure is supported, but you are not forced to use it even if you have native production dependencies.
- Code Signing on a CI server or development machine.
- Auto Update ready application packaging.
- Numerous target formats:
- All platforms:
7z,zip,tar.xz,tar.7z,tar.lz,tar.gz,tar.bz2,dir(unpacked directory). - macOS:
dmg,pkg,mas. - Linux: AppImage, snap, debian package (
deb),rpm,freebsd,pacman,p5p,apk. - Windows:
nsis(Installer),nsis-web(Web installer),portable(portable app without installation), AppX (Windows Store), MSI, Squirrel.Windows.
- All platforms:
- Publishing artifacts to GitHub Releases, Amazon S3, DigitalOcean Spaces and Bintray.
- Advanced building:
- Pack in a distributable format (already packaged app).
- Separate build steps.
- Build and publish in parallel, using hard links on CI server to reduce IO and disk space usage.
- Docker images to build Electron app for Linux or Windows on any platform.
- Downloads all required tool files on demand automatically (e.g., to code sign a Windows application, to make AppX), no setup required.
| Question | Answer |
|---|---|
| “I want to configure electron-builder” | See options |
| “I found a bug or I have a question” | Open an issue |
| “I want to support development” | Donate |
Requirements
Node.js >=22.12.0 is required for v27.
⚠️ Upgrading from v26? v27 is a major release with breaking changes (native ESM, Node >=22.12, and removed deprecated APIs). Start with What's New in v27 for the highlights and the silent default changes, then read the full breaking changes before upgrading.
Then run the automated migration command — it rewrites your config in place:
bashelectron-builder migrate-schemaStep-by-step walkthrough: v26 → v27 migration guide · electron.build/docs/migration/v26-to-v27
Installation
yarn add electron-builder --dev
// or npm, pnpm, bunNote for Yarn 3
Yarn 3 uses PnP by default, but electron-builder still needs node-modules (ref: yarnpkg/berry#4804). Add configuration in the .yarnrc.yaml as follows:
nodeLinker: "node-modules"This instructs Yarn to use node-modules instead of PnP.
Quick Setup Guide
electron-quick-start is the official minimal starter for a new Electron application.
Clone the starter and add electron-builder:
bashgit clone https://github.com/electron/electron-quick-start cd electron-quick-start npm install npm install electron-builder --save-devSpecify the standard fields in the application
package.json—name,description,versionand author.Specify the build configuration in the
package.jsonas follows:json"build": { "appId": "your.id", "mac": { "category": "your.app.category.type" } }See all options. Option files to indicate which files should be packed in the final application, including the entry file, maybe required.
You can also use separate configuration files, such asjs,ts,yml, andjson/json5. See read-config-file for supported extensions. JS Example for programmatic APIAdd icons.
Add the scripts key to the development
package.json:json"scripts": { "app:dir": "electron-builder --dir", "app:dist": "electron-builder" }Then you can run
yarn app:dist(to package in a distributable format (e.g. dmg, windows installer, deb package)) oryarn app:dir(only generates the package directory without really packaging it. This is useful for testing purposes).To ensure your native dependencies are always matched electron version, simply add script
"postinstall": "electron-builder install-app-deps"to yourpackage.json.If you have native addons of your own that are part of the application (not as a dependency), set nativeModules.nodeGypRebuild to
true.
Please note that everything is packaged into an asar archive by default.
For an app that will be shipped to production, you should sign your application. See Where to buy code signing certificates.
Programmatic Usage
TypeScript types are provided and can be found here. See the full programmatic usage guide.
// ESM (recommended)
import { build, Platform } from "electron-builder"
await build({
targets: Platform.MAC.createTarget(),
config: {
// build options — see https://www.electron.build/
}
})// CommonJS — also works on Node >=22.12.0
const { build, Platform } = require("electron-builder")
build({ targets: Platform.MAC.createTarget(), config: {} })
.then(() => { /* done */ })
.catch(console.error)Community Boilerplates
- electron-react-boilerplate A boilerplate for scalable cross-platform desktop apps.
- electron-vue-vite A real simple Electron + Vue3 + Vite5 boilerplate.
- vite-electron-builder Secure boilerplate for Electron app based on Vite. Supports multiple frameworks.
- electronjs-with-nextjs ElectronJS application with NextJS and TypeScript.
Debug
Set the DEBUG environment variable to debug what electron-builder is doing:
DEBUG=electron-builderFPM_DEBUG env to add more details about building linux targets (except snap and appimage).
DEBUG_DMG=true env var to add more debugging/verbosity from hdiutil (macOS).
:::tip[Windows cmd]
On Windows the environment variable is set using the set command.
set DEBUG=electron-builder:::
:::tip[PowerShell]
PowerShell uses different syntax to set environment variables.
$env:DEBUG = "electron-builder":::
If you've identified a bug and want to report it, use electron/minimal-repro to create a minimal reproduction case. Attaching one to your issue significantly speeds up diagnosis.
Donate
This open source work is accomplished during free time. If you'd like to have more time invested in it, please consider donating.
Overview (`.changeset/README.md`)
Changesets
Hello and welcome! This folder has been automatically generated by @changesets/cli, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it in our repository
We have a quick list of common questions to get you started engaging with this project in
our documentation
Overview (`docker/readme.md`)
It is developer documentation. See user documentation.
Build
pnpm docker-images
Notes
We use named data volume instead of mounted host directory to store
node_modulesbecause NPM is unreliable and NPM team doesn't want to fix it.${PWD##*/}-node-modulesis used as name of data volume — it is your current directory name (e. g.foo) and suffix-node-modules.As downside, you have to manually clear outdated data volumes, use
docker volume rm your-name.
Overview (`test/README.md`)
Test file classes: `*Test.ts` vs `*.e2e.ts`
Packaging tests that need the built installer/archive (mount the dmg, list the nupkg, read latest.yml, run the
installer under wine, …) live in *.e2e.ts files, next to the *Test.ts file of the same area (e.g.windows/oneClickInstallerTest.ts + windows/oneClickInstaller.e2e.ts). Everything in a *Test.ts file stops once the
app directory is assembled (a dir target, afterPackTestHook, effectiveOptionComputed, or a thrown configuration
error). Generated toolset suites opt in with e2e: true in their SuiteConfig and are emitted as <suite>__<dims>__e2e.ts
(or <suite>__<dims>.win.e2e.ts when platform-gated).
TEST_MODE selects the class: all (default; CI runs both on every PR and master commit), unit or e2e. TEST_FILES
is a substring override and always wins over the mode. Snapshots follow the file name: foo.e2e.ts →test/snapshots/.../foo.e2e.js.snap.
Inspect output if test uses temporary directory
Set environment variable TEST_APP_TMP_DIR (e.g. /tmp/electron-builder-test).
Specified directory will be used instead of random temporary directory and cleared on each run.
Test Code Signing Ceritificates
If test installer certificate is expired: http://security.stackexchange.com/questions/17909/how-to-create-an-apple-installer-package-signing-certificate
Overview (`packages/app-builder-lib/README.md`)
app-builder-lib
Various utilities. Used by electron-builder.
Overview (`packages/builder-util-runtime/readme.md`)
builder-util-runtime
HTTP utilities. Used by electron-builder.
Overview (`packages/builder-util/readme.md`)
builder-util
Various utilities. Used by electron-builder.
Overview (`packages/dmg-builder/readme.md`)
dmg-builder
Utilities to build DMG. Used by electron-builder.
Overview (`packages/electron-builder-squirrel-windows/readme.md`)
electron-builder-squirrel-windows
Plugin for electron-builder to build Squirrel.Windows installer.
Overview (`packages/electron-forge-maker-appimage/readme.md`)
electron-installer-appimage
electron-builder appimage target for electron-forge.
Recommended to build electron-forge project using electron-builder directly.
Publishing,
Auto Update
and Code Signing supported only in this case
(including all other useful packaging options like files).
Overview (`packages/electron-forge-maker-nsis-web/readme.md`)
electron-forge-maker-nsis-web
electron-builder nsis-web target for electron-forge.
Recommended to build electron-forge project using electron-builder directly.
Publishing,
Auto Update
and Code Signing supported only in this case
(including all other useful packaging options like files).
Overview (`packages/electron-forge-maker-nsis/readme.md`)
electron-forge-maker-nsis
electron-builder nsis target for electron-forge.
Recommended to build electron-forge project using electron-builder directly.
Publishing,
Auto Update
and Code Signing supported only in this case
(including all other useful packaging options like files).
Overview (`packages/electron-forge-maker-snap/readme.md`)
electron-installer-snap
electron-builder snap target for electron-forge.
Recommended to build electron-forge project using electron-builder directly.
Publishing,
Auto Update
and Code Signing supported only in this case
(including all other useful packaging options like files).
Overview (`packages/electron-publish/readme.md`)
electron-publish
Part of electron-builder.
See the Publishing Artifacts for more information.
Can be used standalone.
Overview (`packages/electron-updater/README.md`)
electron-updater
This module allows you to automatically update your application. You only need to install this module and write two lines of code!
To publish your updates you just need simple file hosting, it does not require a dedicated server.
See Auto Update for more information.
Supported OS:
- macOS (Squirrel.Mac).
- Windows (NSIS).
- Linux (AppImage, rpm, deb).
Credits
Thanks to Evolve Labs for donating the npm package name.
Overview (`packages/app-builder-lib/templates/nsis/README.md`)
NSIS
⚠️ It is developer documentation. If your are looking for usage guide, see user documentation.
NSIS stands for Nullsoft Scriptable Install System. electron-builder utilizes a customized version of it and uses NsisMultiUser plugin to handle installation for single user or all users on the computer.
Some links
http://www.mathiaswestin.net/2012/09/how-to-make-per-user-installation-with.html
https://github.com/Drizin/NsisMultiUser
NSIS vs Inno Setup — it is not easy to choose because both are far from ideal, e.g. inno also doesn't have built-in per-user installation implementation — http://stackoverflow.com/questions/34330668/inno-setup-custom-dialog-with-per-user-or-per-machine-installation.
One-click installer: http://forums.winamp.com/showthread.php?t=300479
Localization (l10n)
For translators, the strings to be displayed are included in assistedMessages.yml and [messages.yml]https://github.com/electron-userland/electron-builder/blob/master/packages/app-builder-lib/templates/nsis/messages.yml).
As for other strings in NSIS, head to electron-userland/electron-builder-binaries or the upstream repository on Sorceforge.
GUID
See docs.
We use UUID v5 to generate sha-1 name-based UUID.
http://stackoverflow.com/questions/3029994/convert-uri-to-guid
https://alexandrebrisebois.wordpress.com/2013/11/14/create-predictable-guids-for-your-windows-azure-table-storage-entities/
https://github.com/Squirrel/Squirrel.Windows/pull/658
Compression
NSIS LZMA compression is slower and worse compared to external 7za compression. Slower because 7za is multi-threaded, worse because LZMA codec implementation is outdated and BCJ2 filter is not enabled.
Difference for test app — 4 MB (before: 36.3 after: 32.8).
And compression time is also greatly reduced.
Since NSIS is awesome, no disadvantages in our approach — compression is disabled before File /oname=app.7z "${APP_ARCHIVE}" and enabled after (it is the reasons why SOLID compression is not used).
So, opposite to Squirrel.Windows, archive is not twice compressed.
So, in your custom NSIS scripts you should not use any compression instructions. Only SetCompress if you need to disable compression for already archived file.
Appimage (`website/docs/appimage.md`)
The top-level appImage key contains a set of options instructing electron-builder on how it should build AppImage files.
AppImage is a self-contained, portable Linux application format that packages everything an application needs to run into a single executable file — no installation, no root access required. Under the hood, the application files are stored in a squashfs filesystem that is mounted on launch.
AppImage is one of the two default Linux targets for electron-builder (along with Snap).
AppImage vs. Snap vs. Flatpak vs. DEB
| Aspect | AppImage | Snap | Flatpak | DEB/RPM |
|---|---|---|---|---|
| Installation | None | snap install |
flatpak install |
apt/dnf |
| Sandboxed | No | Yes (strict) | Yes | No |
| Auto-update | AppImageUpdate / electron-updater | Snap Store | Flathub | Package manager |
| Cross-distro | Yes | Yes (snapd needed) | Yes | No |
| Root required | No | No | No | Yes |
| electron-builder default | Yes | Yes | No | No |
How AppImage Works
An AppImage is a single executable file that bundles:
- A runtime binary prepended at the start of the file (handles mounting and launching)
- A squashfs filesystem containing the application and all its dependencies
Running an AppImage
Make the file executable, then run it directly — no installation or root privileges needed:
chmod +x MyApp-1.0.0-x86_64.AppImage
./MyApp-1.0.0-x86_64.AppImageExtracting an AppImage
You can extract the contents of any AppImage without running it:
./MyApp-1.0.0-x86_64.AppImage --appimage-extract
# Contents are extracted to ./squashfs-root/This is useful for inspection, manual desktop integration, or debugging.
Desktop Integration
:::info[Desktop Integration]
Since electron-builder 21, desktop integration (creating .desktop files, associating file types, registering with application menus) is NOT handled by the AppImage itself.
:::
AppImages do not self-integrate with your desktop by default. There are two approaches:
Automatic Integration via AppImageLauncher (recommended)
AppImageLauncher integrates AppImages into your application menu and manages updates. Install it from your distribution's package manager or from its releases page.
Once installed, double-clicking an AppImage will prompt you to integrate it. AppImageLauncher handles:
- Creating a
.desktopentry in~/.local/share/applications/ - Placing icons in
~/.local/share/icons/ - Registering MIME type associations
Manual Integration
Extract the AppImage (see above) and copy the relevant files:
# Extract
./MyApp-1.0.0-x86_64.AppImage --appimage-extract
# Copy desktop entry and icon
cp squashfs-root/myapp.desktop ~/.local/share/applications/
cp squashfs-root/myapp.png ~/.local/share/icons/hicolor/256x256/apps/
# Update the Exec= line in the .desktop file to point to the AppImage
sed -i "s|Exec=.*|Exec=/path/to/MyApp-1.0.0-x86_64.AppImage|" \
~/.local/share/applications/myapp.desktop
# Refresh the desktop database
update-desktop-database ~/.local/share/applications/Desktop Entry Customization
Customize the .desktop file metadata embedded in the AppImage via the linux.desktop option:
linux:
desktop:
entry:
Name: My Application
Comment: A great application
Categories: Office;Productivity
Keywords: productivity;tool
StartupWMClass: myapp
desktopActions:
NewWindow:
Name: New Window
Exec: myapp --new-windowSee Linux Configuration for the full desktop entry reference.
MIME Type Associations
Associate your AppImage with file types via linux.mimeTypes:
linux:
mimeTypes:
- application/x-myapp-documentAuto-Update Support
AppImages support differential (delta) updates via the electron-updater package. electron-builder embeds a blockmap directly into the AppImage binary at build time — no separate file needs to be published alongside it. electron-updater reads the embedded blockmap to determine which blocks have changed and downloads only those blocks. Configure a publish provider and electron-updater will handle updates:
publish:
provider: github # or s3, generic, etc.See Auto Update for the full setup guide.
Architecture Support
| Architecture | CLI Flag | Notes |
|---|---|---|
x64 |
--x64 |
Standard 64-bit x86 Linux |
arm64 |
--arm64 |
64-bit ARM (Raspberry Pi 4+, server ARM) |
armv7l |
--armv7l |
32-bit ARM (Raspberry Pi 3 and older) |
Build requirements: Linux host (or Docker) for all architectures.
Toolsets
The AppImage build tools are configured via the top-level toolsets.appimage option. As of v27 the default resolves to the static, FUSE3-compatible runtime; the legacy FUSE2 runtime is available by pinning "0.0.0".
toolsets.appimage |
Runtime | Notes |
|---|---|---|
unset / "latest" → 1.1.0 |
Static runtime (no host FUSE needed) | Default in v27 |
"0.0.0" |
Legacy FUSE2 | Opt-in only — requires libfuse.so.2 on the host |
Default — static runtime
The default toolset bundles a static AppImage runtime that does not depend on FUSE2. The runtime is prepended directly to the squashfs filesystem and handles mounting internally, so AppImages run on modern distributions (Arch, Fedora, Ubuntu 24.04+) without installing FUSE. Its AppRun entry point also performs smart sandbox detection — it passes --no-sandbox to Electron only when unprivileged user namespaces are unavailable, rather than always.
:::note[v27: default --no-sandbox change]
With the legacy FUSE2 runtime (toolsets.appimage: "0.0.0"), electron-builder injects --no-sandbox by default. With the default static runtime it does not — AppRun adds it only when user namespaces are unavailable. Set executableArgs: ["--no-sandbox"] if you need to force it.
:::
Legacy runtime (`0.0.0`) — FUSE2
Pin toolsets: { appimage: "0.0.0" } only if you specifically need the original FUSE2 runtime. FUSE2 has been unmaintained since 2017 and is increasingly unavailable on modern distributions, where an AppImage built with it fails to launch:
dlopen(): error loading libfuse.so.2
AppImages require FUSE to run.toolsets:
appimage: "0.0.0" # legacy FUSE2 runtime — only if you need itCompression
AppImage files use squashfs internally. The compression algorithm affects file size and launch speed.
:::info[Compression support by toolset]
- Legacy FUSE2 (
0.0.0): an explicitappImage.compressionofxzorgzipis forwarded to mksquashfs; otherwise the rootcompression: "maximum"selectsxz. All other values use mksquashfs's default (gzip). - Static runtime (default): supports
gzipandzstd. TheappImage.compressionoption selects the algorithm directly;"xz"is mapped to"zstd"(xz is not compiled into the static runtime binary).
:::
| Algorithm | File size | Decompression speed | Notes |
|---|---|---|---|
zstd |
Moderate | Fast | Default for static runtime |
gzip |
Moderate | Moderate | Legacy mksquashfs default |
xz |
Smallest | Slowest | Mapped to zstd on static runtime |
Direct algorithm selection (the legacy 0.0.0 toolset forwards only gzip and xz):
appImage:
compression: zstd # gzip | zstd | xz (mapped to zstd)License Agreement (EULA)
If your application requires users to accept a license before first launch, set the license option to the path of a .txt or .html file:
appImage:
license: build/EULA.txtThe license dialog is shown on first launch using zenity, kdialog, or Xdialog (whichever is available). Acceptance is recorded in $XDG_CONFIG_HOME//eulaAccepted.
Build Requirements
AppImages must be built on Linux (or via Docker). They cannot be cross-compiled from macOS or Windows.
To build on a non-Linux host:
docker run --rm -ti \
--env-file <(env | grep -iE 'DEBUG|NODE_|ELECTRON_|YARN_|NPM_|CI|CIRCLE|TRAVIS_TAG|TRAVIS|TRAVIS_REPO_|TRAVIS_BUILD_|TRAVIS_BRANCH|TRAVIS_PULL_REQUEST_|APPVEYOR_|CSC_|GH_|GITHUB_|BT_|AWS_|STRIP|BUILD_') \
--env ELECTRON_CACHE="/root/.cache/electron" \
--env ELECTRON_BUILDER_CACHE="/root/.cache/electron-builder" \
-v ${PWD}:/project \
-v ~/.cache/electron:/root/.cache/electron \
-v ~/.cache/electron-builder:/root/.cache/electron-builder \
electronuserland/builder:wine \
/bin/bash -c "npm install && npx electron-builder --linux"See Multi Platform Build for full Docker documentation.
Debugging
Enable verbose output:
DEBUG=electron-builder electron-builder --linux AppImageTest the built AppImage:
chmod +x dist/MyApp-1.0.0.AppImage
./dist/MyApp-1.0.0.AppImage --no-sandboxThe --no-sandbox flag may be required on some distributions where user namespaces are restricted.
Configuration
{!./app-builder-lib.Interface.AppImageOptions.md!}
Appx (`website/docs/appx.md`)
The top-level appx key contains a set of options instructing electron-builder on how it should build AppX packages for the Windows Store or enterprise sideloading.
All options are optional. All required AppX configuration is inferred and computed automatically.
When to Use AppX
AppX is the packaging format for the Microsoft Store (also called MSIX). Choose AppX when:
- Windows Store distribution — your app will be listed in the Microsoft Store
- Enterprise sideloading — IT manages app distribution via MDM/Intune without the Store
- Windows 10S / Windows SE — these locked-down Windows editions only run Store apps
For standard consumer distribution, NSIS is simpler. For enterprise deployment without the Store, MSI is often preferred.
AppX vs. NSIS vs. MSI
| Aspect | AppX | NSIS | MSI |
|---|---|---|---|
| Windows Store | Yes | No | No |
| Sideloading | Yes (signed) | N/A | N/A |
| Enterprise MDM | Limited | No | Yes |
| Sandboxed | Partial | No | No |
| Auto-update | Store only (not electron-updater) | electron-updater | Not supported via electron-updater |
| Code signing | Required (or Store) | Recommended | Recommended |
Code Signing {#appx-package-code-signing}
- Store distribution — no manual signing needed. The Windows Store signs the package with a Microsoft certificate during the submission process.
- Sideloading / enterprise — the AppX must be signed with a trusted certificate. See Windows Code Signing.
For self-signed certificates during development, see Microsoft's self-signed certificate guide.
AppX Assets
AppX requires specific logo/icon assets. Place them in the appx folder inside your build resources directory (default: build/appx/):
| Asset | File Name | Required | Size |
|---|---|---|---|
| Store Logo | StoreLogo.png |
Yes | 50×50 |
| Square 150×150 | Square150x150Logo.png |
Yes | 150×150 |
| Square 44×44 | Square44x44Logo.png |
Yes | 44×44 |
| Wide 310×150 | Wide310x150Logo.png |
Yes | 310×150 |
| Badge Logo | BadgeLogo.png |
Optional | 24×24 |
| Large Tile | LargeTile.png |
Optional | 310×310 |
| Small Tile | SmallTile.png |
Optional | 71×71 |
| Splash Screen | SplashScreen.png |
Optional | 620×300 |
Default assets are used for the required logos if you don't provide your own. Scale variants (e.g., Square44x44Logo.scale-200.png) are supported — see Microsoft's tile asset guidelines.
Identity and Publisher
appx:
applicationId: "MyApp" # must be alphanumeric, no spaces
identityName: "MyCompany.MyApp" # unique within the Store
publisher: "CN=My Company, O=My Company, C=US" # must match certificate Subject
publisherDisplayName: "My Company"
displayName: "My Application" # displayed in Start menu:::warning[Publisher Must Match Certificate]
The publisher value must exactly match the Subject field of your code signing certificate. Mismatches cause installation failures.
:::
Capabilities
AppX apps declare the system capabilities they use. Specify capabilities in appx.capabilities:
appx:
capabilities:
- runFullTrust # default — allows unrestricted execution
- internetClient # outbound internet access
- privateNetworkClientServer # local network
- webcam # camera access
- microphone # microphone accessrunFullTrust is required for most Electron apps (included by default). The full list of capabilities is in Microsoft's documentation.
Tile Customization
appx:
backgroundColor: "#2c2c2c" # tile background color (CSS color)
showNameOnTiles: false # show app name on tile (default: false)Language Support
appx:
languages:
- en-US
- de-DE
- fr-FR
- ja-JPDeclare which languages your app supports. This affects Store listings and localized resources.
Version Targeting
appx:
minVersion: "10.0.17763.0" # Windows 10 1809 minimum
maxVersionTested: "10.0.22621.0" # Windows 11 22H2 tested maximumAuto-Launch Extension
appx:
addAutoLaunchExtension: false # Default: falseWhen true, adds a startup task extension that runs the app on user login. Requires the runFullTrust capability.
Custom Manifest
For advanced scenarios, provide a completely custom AppX manifest:
appx:
customManifestPath: build/AppxManifest.xmlOr inject additional XML extensions:
appx:
customExtensionsPath: build/appx-extensions.xmlPublishing to the Windows Store
- Create a Microsoft Developer account ($19 one-time fee)
- Reserve your app name in Partner Center
- Build your AppX:
electron-builder --win appx - Test the package locally (see Testing below)
- Upload the
.appxfile via Partner Center submission - Complete the submission (age rating, pricing, screenshots, etc.)
- Wait for Store certification (typically 1-3 business days)
Sideloading Without the Store
For enterprise distribution without the Store:
# Enable sideloading (Windows 10 1803+, this is the default)
# On Windows 10 S/SE, you must use the Store
# Install the AppX
Add-AppxPackage -Path ".\MyApp-1.0.0.appx"If using a self-signed certificate, the user must trust it first (see "Common Questions" below).
Testing Locally
# Install locally (Developer Mode must be enabled for unsigned packages)
Add-AppxPackage -Path ".\MyApp-1.0.0.appx"
# Uninstall
Get-AppxPackage | Where-Object {$_.Name -like "*MyApp*"} | Remove-AppxPackageCommon Questions
How do I install an AppX with a self-signed certificate?
Import the certificate into "Trusted People" on the target machine — see Microsoft's guide to signing MSIX packages. Then install the AppX normally with Add-AppxPackage.
Does AppX support auto-updates without the Store?
No — AppX auto-updates are handled by the Windows Store only. electron-updater does not support AppX packages. For auto-updates outside the Store, use NSIS instead.
Configuration
{!./app-builder-lib.Interface.AppXOptions.md!}
Architecture (`website/docs/architecture.md`)
Architecture & Multi-Arch Builds
electron-builder can produce builds for multiple CPU architectures. This page covers architecture support by platform, how to build universal macOS binaries, and cross-compilation considerations.
Supported Architectures by Platform
| Platform | Architecture | CLI Flag | Notes |
|---|---|---|---|
| macOS | x64 | --x64 |
Intel Macs |
| macOS | arm64 | --arm64 |
Apple Silicon (M1/M2/M3/M4) |
| macOS | universal | --universal |
Runs natively on both Intel and Apple Silicon |
| Windows | x64 | --x64 |
Most common desktop target |
| Windows | ia32 | --ia32 |
32-bit; legacy support. Requires electronVersion <= 43.x — Electron 44 removed Windows ia32 builds |
| Windows | arm64 | --arm64 |
Windows on ARM (Surface Pro X, etc.) |
| Linux | x64 | --x64 |
Standard 64-bit x86 Linux |
| Linux | arm64 | --arm64 |
Raspberry Pi 4+, ARM servers, cloud instances |
| Linux | armv7l | --armv7l |
Raspberry Pi 3 and older, 32-bit ARM. Requires electronVersion <= 43.x — Electron 44 removed Linux armv7l builds |
:::note[32-bit support ends with Electron 43]
Electron 44 removed Windows ia32 and Linux armv7l builds (electron/electron#51816). To keep shipping those architectures, stay on Electron <= 43.x — the v43 series is supported until its end-of-life in January 2027. electron-builder fails fast with a configuration error when such a build is requested against Electron 44+ (downgraded to a warning when a custom electronDist or Electron mirror is configured).
:::
Specifying Architecture
CLI
# Build for a specific architecture
electron-builder --mac --x64
electron-builder --mac --arm64
electron-builder --mac --universal
# Build multiple architectures in sequence
electron-builder --win --x64 --arm64
# Build for all configured targets and architectures
electron-builder --mac --win --linuxConfiguration
mac:
target:
- target: dmg
arch:
- x64
- arm64
- universal
- target: zip
arch: universal:::note[v27: arch: "all" now expands to x64 + arm64]
On Windows and Linux, arch: "all" (and the default multi-arch build) expands to x64 + arm64 in v27 — it no longer includes ia32. Request ia32 (or armv7l) explicitly if you still build 32-bit. See What's New in v27.
:::
macOS: Universal Binaries
A universal binary contains both x64 and arm64 slices in a single executable. It runs natively on Intel Macs and Apple Silicon without Rosetta 2 translation.
Building Universal
electron-builder --mac --universalOr set in configuration:
mac:
target:
- target: dmg
arch: universalelectron-builder downloads both x64 and arm64 Electron binaries, then merges them using lipo.
How ASAR Merging Works
When building a universal binary, electron-builder must merge two ASAR archives (one per architecture). This works automatically for pure JavaScript. For native modules:
mac:
universal: # v27: universal-build options live under mac.universal
mergeASARs: true # Default: true — merge the two per-arch ASARs into one
singleArchFiles: "" # Glob of files that must NOT be merged (arch-specific binaries)
x64ArchFiles: "" # Glob of files that exist only in the x64 buildArch-Specific Native Modules
If you have native modules that differ between x64 and arm64, you must configure which files should remain architecture-specific:
mac:
universal:
mergeASARs: true
singleArchFiles: "node_modules/canvas/**" # only exists in x64For modules that exist in both architectures but have different binaries:
mac:
universal:
mergeASARs: true
x64ArchFiles: "node_modules/my-module/build/Release/my_module.node"Verifying a Universal Binary
# Check that both slices are present
lipo -info dist/mac-universal/MyApp.app/Contents/MacOS/MyApp
# Should output: Architectures in the fat file: x64 arm64
# Check a framework
lipo -info "dist/mac-universal/MyApp.app/Contents/Frameworks/Electron Framework.framework/Electron Framework"Universal vs. Separate Builds
| Approach | Pros | Cons |
|---|---|---|
Universal (universal) |
Single download for all users | ~2x file size |
Separate (x64 + arm64) |
Smaller download per user | Two separate release artifacts |
Most consumer apps ship universal binaries for the best user experience. If binary size is critical, ship separate architectures.
Windows ARM64
Windows ARM64 builds run natively on ARM-based Windows devices (Surface Pro X, Snapdragon-based laptops, etc.).
electron-builder --win --arm64:::note[Cross-compilation from x64]
Windows ARM64 builds can be cross-compiled from an x64 Windows machine. The Electron binary for ARM64 is downloaded automatically.
:::
Linux ARM
Linux ARM builds are commonly used for:
- Raspberry Pi —
armv7l(Pi 3 and older) orarm64(Pi 4+) - ARM servers —
arm64(AWS Graviton, Ampere Altra, etc.) - Embedded Linux —
armv7l
# For Raspberry Pi 4+ and ARM servers
electron-builder --linux --arm64
# For older Raspberry Pi (32-bit OS)
electron-builder --linux --armv7lBuilding Linux ARM on x64
Linux ARM can be built on an x64 machine using QEMU emulation via Docker:
# Install QEMU support (one-time setup)
docker run --privileged --rm tonistiigi/binfmt --install arm64,arm
# Build ARM64 AppImage
docker run --rm \
-v ${PWD}:/project \
-v ~/.cache/electron:/root/.cache/electron \
-v ~/.cache/electron-builder:/root/.cache/electron-builder \
--platform linux/arm64 \
electronuserland/builder \
/bin/bash -c "npm ci && npx electron-builder --linux --arm64"Cross-Platform Builds
Building Windows on Linux/macOS
Use the Wine-based Docker image:
docker run --rm \
-v ${PWD}:/project \
-v ~/.cache/electron:/root/.cache/electron \
-v ~/.cache/electron-builder:/root/.cache/electron-builder \
electronuserland/builder:wine \
/bin/bash -c "npm ci && npx electron-builder --win"Wine is used to run NSIS (which generates the .exe installer). The Electron binary itself is compiled natively — Wine is only needed for the installer tool.
Building Linux on macOS
Linux builds require either a native Linux machine or Docker:
docker run --rm \
-v ${PWD}:/project \
-v ~/.cache/electron:/root/.cache/electron \
-v ~/.cache/electron-builder:/root/.cache/electron-builder \
electronuserland/builder \
/bin/bash -c "npm ci && npx electron-builder --linux"Building AppX on macOS (via Parallels VM)
For AppX (Windows Store), MakeAppx.exe requires Windows. When building on macOS with Parallels Desktop (Pro Edition), electron-builder automatically detects and uses the running Windows VM.
See Multi Platform Build for full Docker documentation.
Native Modules and Architecture
Native modules (.node files) are compiled for a specific architecture and cannot be used in a different architecture without recompilation.
In practice, electron-builder handles this automatically during the build process when nativeModules.npmRebuild: true (the default; in v27 the native-module options npmRebuild, nodeGypRebuild, buildDependenciesFromSource, and rebuildMode live under nativeModules).
:::note[v27: node_modules are arch/os-filtered on every build]
v27 filters node_modules by each package's package.json cpu / os fields against the target arch and platform on every build (previously this effectively only mattered for universal macOS builds). A dependency that declares an incompatible cpu/os for the target is excluded from the packaged app. This is usually what you want; if you intentionally bundle a cross-arch/cross-os optional binary, include it explicitly via extraResources / files.
:::
Universal Binary with Native Modules
Building a universal macOS binary with native modules requires that each native module has both x64 and arm64 variants. If a module only has one architecture, you'll need to use singleArchFiles to keep it arch-specific:
mac:
universal:
mergeASARs: true
singleArchFiles: "node_modules/serialport/**/*.node"Or if you can compile the module for both architectures, the universal build will merge them automatically via lipo.
Architecture in GitHub Actions
Build multiple architectures in parallel using a matrix:
jobs:
build:
strategy:
matrix:
include:
- os: macos-latest
args: --mac --universal
- os: windows-latest
args: --win --x64 --arm64
- os: ubuntu-latest
args: --linux --x64
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npx electron-builder ${{ matrix.args }} --publish always
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}Related Pages
- GitHub Actions — full CI/CD workflows
- Multi Platform Build — Docker images and cross-compilation
- macOS Configuration —
mac.universal.mergeASARs,mac.universal.singleArchFiles,mac.universal.x64ArchFiles - Target Selection — choosing the right target format per platform
Cli (`website/docs/cli.md`)
title: CLI
/* Detailed source-code truncated for AI context efficiency. */For other commands please see help using --help arg, e.g. ./node_modules/.bin/electron-builder install-app-deps --help
:::tip
npx is bundled with Node.js, so you can simply use npx electron-builder.
:::
:::note[Requirements]
electron-builder v27 requires Node.js >=22.12.0. Upgrading from v26? Review the v27 breaking changes, then follow the migration walkthrough.
:::
`migrate-schema`
New in v27. Rewrites your build configuration from the v26 shape to the v27 shape in place, applying every config-level breaking change (see the walkthrough).
electron-builder migrate-schema # apply changes in place
electron-builder migrate-schema --dry-run # preview without writing (alias: -n)- Auto-detects your config: the
package.jsonbuildkey, orelectron-builder.{yml,yaml,json,json5,toml,js,cjs,mjs,ts}. Pass--configfor a non-default file and--project-dir <dir>for the project root. - Rewrites static configs (
json/json5/yaml/package.json) and programmatic ones (.js/.ts/.cjs/.mjs, via an AST codemod that preserves comments and formatting when the config reduces to a single object literal). - TOML is detected but not rewritten (the
tomllibrary is read-only) — it prints the required changes for you to apply. JSON5 is re-serialized as JSON (comments are not preserved). - Prints a warning for anything it cannot rewrite mechanically (e.g.
squirrelWindows.customSquirrelVendorDir) and an advisory for runtime defaults and behavior changes worth checking (annsis-webtarget, a per-machinensis/nsisWebinstaller, a customwin.sign.signhook without a publisher name, agenericpublishurlwith a query string, macOS configs relying on the default entitlements). Advisories never change the file. See what it does and does not do.
:::note[Removed CLI flags]
The v22-era --em.build / --em.directories flags were removed — pass build config inline with -c (e.g. -c.directories.output=dist). Implicit publishing was also removed: pass --publish explicitly.
:::
Prepend npx to sample commands below if you run them from Terminal and not from package.json scripts.
:::note[build for macOS, Windows and Linux]electron-builder -mwl
:::
:::note[build deb and tar.xz for Linux]electron-builder --linux deb tar.xz
:::
:::note[build NSIS 32-bit installer for Windows (Electron <= 43 only)]electron-builder --windows nsis:ia32
Electron 44 removed Windows ia32 builds — ia32 requires electronVersion <= 43.x (supported until the v43 series reaches end-of-life in January 2027).
:::
:::note[set package.json property foo to bar]electron-builder -c.extraMetadata.foo=bar
:::
:::note[configure unicode options for NSIS]electron-builder -c.nsis.unicode=false
:::
Target
Without target configuration, electron-builder builds Electron app for current platform and current architecture using default target.
- macOS - DMG and ZIP for Squirrel.Mac.
- Windows - NSIS.
- Linux:
Platforms and archs can be configured using CLI args, or in the configuration.
For example, if you don't want to pass --x64 and --arm64 flags each time, but instead build by default NSIS target for all archs for Windows (add ia32 only when staying on Electron <= 43):
:::note[Configuration]
package.json
"build": {
"win": {
"target": [
{
"target": "nsis",
"arch": [
"x64",
"arm64"
]
}
]
},
"mac": {
"target": [
{
"target": "dmg",
"arch": [
"universal"
]
}
]
}
}electron-builder.yml
win:
target:
- target: nsis
arch:
- x64
- arm64
mac:
target:
- target: dmg
arch: universalelectron-builder.config.js
module.exports = {
"win": {
"target": [
{
"target": "nsis",
"arch": [
"x64",
"arm64"
]
}
]
},
"mac": {
"target": [
{
"target": "dmg",
"arch": [
"universal"
]
}
]
}
}:::
and use
build -wlTargetConfiguration
targetString - The target name. e.g.snap.arch"x64" | "ia32" | "armv7l" | "arm64" | "universal" - The arch or list of archs.
Configuration (`website/docs/configuration.md`)
title: "Configuration"
{!./partials/_upgrading-from-v26.md!}
electron-builder configuration can be defined
in the
package.jsonfile of your project using thebuildkey on the top level:json"build": { "appId": "com.example.app" }or through the
--configoption. Defaults toelectron-builder.yml.yamlappId: "com.example.app"json, json5, toml orjs/ts(exported configuration or function that produces configuration) formats are also supported.:::tip
If you want to use ajsfile, do not name itelectron-builder.js. It will conflict with theelectron-builderpackage name.
::::::tip
If you want to use toml, install it withnpm install toml --save-dev.
:::
Most of the options accept null — for example, to explicitly set that DMG icon must be default volume icon from the OS and default rules must be not applied (i.e. use application icon as DMG icon), set dmg.icon to null.
Artifact File Name Template
${ext} macro is supported in addition to file macros.
Environment Variables from File
Env file electron-builder.env in the current dir (example). Supported only for CLI usage.
Effective Configuration
Before packaging, electron-builder writes the resolved configuration to builder-effective-config.yaml in the output directory (directories.output, dist by default). It is the merged result of package.json, the config file, presets and CLI overrides, plus values detected during the build such as electronVersion. Sensitive fields (passwords, tokens, cscLink) are masked.
By default the file is written only for local interactive builds, not on CI or when stdout is piped. Set writeEffectiveConfig to true to always write it (e.g. electron-builder -c.writeEffectiveConfig=true in a CI step), or to false to never write it.
How to Read Docs
- Name of optional property is normal, required is bold.
- Type is specified after property name:
Array<String> | String. Union like this means that you can specify or string (**/*), or array of strings (["**/*", "!foo.js"]).
Common Configuration
{!./app-builder-lib.Interface.CommonConfiguration.md!}
Overridable per Platform Options
Following options can be set also per platform (top-level keys mac, linux and win) if need.
Base Configuration
{!./app-builder-lib.Interface.PlatformSpecificBuildOptions.md!}
Metadata
Some standard fields should be defined in the package.json.
{!./app-builder-lib.Interface.Metadata.md!}
Electron Download & Mirror
electron-builder downloads the Electron distribution to package via @electron/get. Point it at a mirror (e.g. a corporate proxy or a China mirror) with the electronGet option:
{
"build": {
"electronGet": {
"mirrorOptions": { "mirror": "https://npmmirror.com/mirrors/electron/" }
}
}
}:::note[v27: renamed from electronDownload]
This key was electronDownload in v26. In v27 it is electronGet, reshaped to @electron/get v5 options: the flat mirror moved under mirrorOptions.mirror, isVerifyChecksum: false became unsafelyDisableChecksums: true, and cache / customDir / customFilename / strictSSL / force were dropped. electron-builder migrate-schema performs the rename. See v27 Breaking Changes → electronGet.
:::
Build Version Management
CFBundleVersion (macOS) and FileVersion (Windows) will be set automatically to version.build_number on CI server (Travis, AppVeyor, CircleCI and Bamboo supported).
Build Hooks
{!./features/hooks.md!}
Contents (`website/docs/contents.md`)
Application Contents
This page covers how electron-builder determines which files are included in the packaged app and how to customize file inclusion.
How electron-builder Collects Files
electron-builder uses a two-phase approach to build the app bundle:
- App files — your application source code and assets, placed inside the ASAR archive (or directly in the app folder if ASAR is disabled)
- Extra files/resources — additional files copied into specific locations in the output package, outside the ASAR archive
Default File Inclusion
By default, electron-builder includes all files matching **/* from the app directory, minus a comprehensive set of automatically-excluded files:
Always excluded:
node_modulesdev dependencies (only production dependencies are included)*.iml,*.o,*.hprof,*.pyc,*.pyo,*.rbc,*.swp,.csproj,.sln,.slnx,.xproj.editorconfig,._*,.DS_Store,.git,.hg,.svn,.gitignore,.gitattributes__pycache__,.flowconfig,.idea,.vs,.nyc_outputappveyor.yml,.travis.yml,circle.ymlnpm-debug.log,yarn.lock,.yarn-integrity,.yarn-metadata.json- Test directories:
{test,__tests__,tests,powered-test,example,examples}inside node_modules *.d.tsfiles inside node_modulesnode_modules/.bin
These exclusions are always applied even when you define custom patterns.
The `files` Option
Override which files are included using the files option. Patterns are glob expressions relative to the app directory:
files:
- "**/*"
- "!src/**" # exclude source files
- "!tests/**" # exclude test directory
- "!**/*.map" # exclude source maps
- "dist/**" # include dist directoryImportant behavior:
- If any pattern does NOT start with
!(i.e., it's an include pattern), the default**/*pattern is NOT automatically prepended. You must include it explicitly if you want to start from all files. package.jsonandnode_modules/**/*(production only) are always included regardless of patterns.- All default ignores (listed above) are always applied.
Simple Include Example
files:
- "**/*"
- "!src/**"
- "!*.ts"
- "!tsconfig*.json"Only Include Specific Directories
files:
- dist/**
- assets/**
- node_modules/** # production node_modules (already automatic)
- package.json # always included, but can be explicitExclude Large Unused Files
files:
- "**/*"
- "!node_modules/**/README*"
- "!node_modules/**/*.md"
- "!node_modules/**/*.map"
- "!**/test/**"
- "!**/__tests__/**"FileSet Objects
For advanced scenarios, use FileSet objects instead of (or alongside) glob strings. FileSets let you copy files from a different source directory:
files:
- from: dist
to: .
filter:
- "**/*"
- "!**/*.map"
- from: assets
to: assets| Field | Description |
|---|---|
from |
Source path. Relative to the app directory for files; relative to project dir for extraResources/extraFiles. Defaults to the app directory. |
to |
Destination path. Relative to the ASAR root for files; relative to content dir for extraFiles; relative to resources dir for extraResources. |
filter |
Glob patterns relative to from. Defaults to **/*. |
Renaming a File
files:
- from: dist/main.js
to: app.jsCopying from Outside the App Directory
files:
- "**/*"
- from: ../shared/assets
to: shared-assetsExtra Files and Resources
Copy files into the packaged app outside the ASAR archive:
`extraResources`
Copies into the resources/ directory:
- macOS:
MyApp.app/Contents/Resources/ - Windows/Linux:
resources/
extraResources:
- from: native-binaries/
to: bin
filter:
- "**/*"
- "assets/icon.png" # string shorthand — copies to resources/assets/icon.png`extraFiles`
Copies into the app content directory:
- macOS:
MyApp.app/Contents/ - Windows/Linux: the install directory root
extraFiles:
- from: config/
to: config
- "LICENSE":::tip[Native Modules and Extra Binaries]
Use extraResources for native binaries, CLI tools, or data files that need to be accessible at runtime. Access them in your app via process.resourcesPath.
:::
ASAR Packaging
ASAR is Electron's archive format — it bundles all app files into a single app.asar file for faster loading and to prevent casual file inspection.
asar: {} # ASAR is enabled by default — pass an object to configure, or set asar: false to disableSet asar: false to disable ASAR packaging (all files are placed in the app directory directly). Useful for debugging or if you have unusual file access patterns.
Unpacking Files from ASAR
Some files cannot be inside an ASAR archive:
- Native modules (
.nodefiles) - Large binary assets that need random-access reads
- Files that need to be executed directly
Use asar.unpack to unpack specific files into app.asar.unpacked/:
asar:
unpack:
- "**/*.node" # all native modules
- "resources/ffmpeg" # large binary
- "**/**/node_modules/sharp/**" # specific moduleFiles in app.asar.unpacked/ are accessible via the same paths as if they were in the ASAR — Electron transparently redirects reads.
Smart Unpack
asar:
smartUnpack: true # Default: trueWhen enabled, electron-builder automatically detects executables and native modules and unpacks them from ASAR. You generally don't need to configure asar.unpack manually unless you have unusual cases.
ASAR Ordering
Optimize app startup time by specifying file ordering within the ASAR archive. Files listed first are packed first, enabling faster sequential reads at startup:
asar:
ordering: build/asar-ordering.txt# build/asar-ordering.txt — list files loaded at startup first
index.js
renderer.js
node_modules/electron/index.jsPractical Examples
Electron App with Native Modules
files:
- "**/*"
- "!src/**"
- "!**/*.ts"
- "!**/*.map"
asar:
unpack:
- "**/*.node"App with Large Video Assets
files:
- "**/*"
- "!src/**"
- "!videos/**" # exclude from ASAR
extraResources:
- from: videos/
to: videos # place outside ASAR, accessible via process.resourcesPathMonorepo Setup (Two-Package Structure)
directories:
app: packages/app # app directory is not the project root
files:
- "**/*"
- "!src/**"See Two package.json Structure for the full monorepo setup.
Including Default-Excluded Files
electron-builder excludes a set of development-only file extensions and names by default — for example
build artifacts such as .obj, .o, .a, and .d.ts, lockfiles such as yarn.lock andpackage-lock.json, and VCS/tooling metadata such as .git, .idea, and .github.
To ship a file that would otherwise be excluded, add an explicit files glob that targets it. An
explicit include overrides the matching default exclusion:
files:
- "**/*"
- "**/*.obj" # keep Wavefront .obj 3D models that are excluded by defaultThe same applies to default-excluded names — reference the name in a pattern segment, e.g.:
files:
- "**/*"
- "**/.github/**" # keep the .github directory that is excluded by defaultOnly patterns that name an extension or directory concretely opt it back in; broad patterns such as**/* continue to honor the defaults.
Troubleshooting
File is missing from packaged app: Check if the file is being excluded by a pattern. Run DEBUG=electron-builder electron-builder build to see detailed file collection output.
Native module crashes at runtime: The module likely needs to be outside the ASAR archive. Add it to asar.unpack:
asar:
unpack:
- "node_modules/better-sqlite3/**"App directory is too large: Profile what's included by examining the app bundle after building. Common culprits: source maps (*.map), TypeScript source files (*.ts), and large test fixtures.
FileSet from path not found: Remember that from in files is relative to the app directory. For extraResources/extraFiles, it's relative to the project root.
File Contents Configuration
{!./app-builder-lib.Interface.FilesBuildOptions.md!}
FileSet Configuration
{!./app-builder-lib.Interface.FileSet.md!}
Dmg (`website/docs/dmg.md`)
title: "DMG"
The top-level dmg key contains a set of options instructing electron-builder on how it should build DMG (macOS disk image) files.
When to Use DMG
DMG is the standard distribution format for direct-download macOS apps. Users mount the disk image, drag the app to their Applications folder, and eject the image. Use DMG when:
- Distributing directly from your website or GitHub Releases
- Using electron-updater (the
ziptarget also runs alongside DMG) - You want the most familiar macOS install experience
For Mac App Store distribution, see MAS. For system-level installs (kernel extensions, launch daemons), see PKG.
Window Customization
The window option controls the Finder window that appears when the DMG is mounted:
dmg:
window:
position:
x: 130
y: 220
size:
width: 540
height: 380Standard DMG windows are 540×380 pixels. The position coordinates place the window on screen — x: 130, y: 220 is a common convention.
Background Image
Set a background image to make your DMG installer look polished:
dmg:
background: build/background.pngRequirements:
- Format: PNG (recommended for quality and transparency support)
- Size: match your
window.size— typically 540×380 pixels - For Retina displays: provide a
@2xversion at 1080×760 and name it[email protected]. electron-builder will automatically use the high-resolution version on Retina screens.
If no background image is set, use backgroundColor to set a solid color:
dmg:
backgroundColor: "#2c2c2c"Icon Layout
The contents array defines the position and type of icons inside the DMG window. A typical layout has the app icon on the left and a /Applications shortcut on the right:
dmg:
contents:
- x: 130
y: 220
type: file
- x: 410
y: 220
type: link
path: /Applications| Field | Description |
|---|---|
x, y |
Position in the DMG window (pixels from top-left) |
type |
file — the app itself; link — a symlink (e.g., to /Applications) |
path |
Only for link type — the symlink destination |
name |
Optional display name override |
:::tip[Centering Icons]
For a 540×380 window with two icons, the standard horizontal positions are x=130 (app) and x=410 (Applications link), both at y=220 (vertically centered).
:::
Volume Icon and Title
The DMG volume appears in the macOS Finder sidebar when mounted. Customize its appearance:
dmg:
# Badge icon shown in Finder when the DMG is mounted
badgeIcon: build/icon.icns
# Volume icon (what appears as the disk image itself in Finder)
icon: build/volume-icon.icns
# Title shown in the Finder window title bar when mounted
title: "${productName} ${version}"
# Icon size inside the DMG window
iconSize: 80
# Font size for icon labels
iconTextSize: 12The default title is ${productName} ${version}. The default iconSize is 80 pixels.
Disk Image Format
The format option controls the compression algorithm:
| Format | Description | Use Case |
|---|---|---|
UDZO |
zlib-compressed (default) | Good balance of size and compatibility |
ULFO |
LZFSE-compressed | Faster decompression, macOS 10.11+ only |
ULMO |
LZMA-compressed | Best compression, slower to mount, macOS 10.15+ only |
UDBZ |
bzip2-compressed | Better compression than UDZO, slower |
UDRO |
Read-only, uncompressed | Largest size, fastest to open |
UDRW |
Read-write | Only for development/testing |
For most cases, leave format at the default UDZO. Use ULFO if you are targeting macOS 10.11+ and want faster mount times for large apps. Use ULMO if you are targeting macOS 10.15+ and want the smallest download — it typically compresses an Electron app's DMG ~30% smaller than UDZO, at the cost of a few extra seconds to mount and copy at install time.
Volume Filesystem
The filesystem option controls the filesystem of the DMG volume:
| Value | Notes |
|---|---|
APFS |
Default as of v27. The modern macOS filesystem — smaller, faster-to-mount images on current macOS. Cannot be mounted on macOS before 10.13 (High Sierra). |
HFS+ |
The pre-v27 default. Set this only if you must support pre-10.13 macOS. |
dmg:
filesystem: "HFS+" # only for pre-10.13 (High Sierra) compatibility:::note[v27 default change]
The default changed from HFS+ to APFS in v27. This is a runtime default, not a config-key rename, so migrate-schema does not change it — set filesystem: "HFS+" explicitly if you need the old behavior. See v27 Breaking Changes → DMG filesystem.
:::
DMG Size
The size option sets the initial filesystem size. Normally this is computed automatically — only set it if you encounter errors about insufficient space during DMG creation:
dmg:
size: "500m"Set shrink: false to disable automatic shrinking of the DMG to the minimum size after creation (not recommended for production).
Signing the DMG
The DMG itself can be signed separately from the app bundle inside it:
dmg:
sign: true # Default: false:::note
The app bundle inside the DMG is always signed (when signing is configured). The sign option here refers to signing the DMG container file itself. Signing the DMG is not required for Gatekeeper but may be requested in some enterprise environments.
:::
Internet-Enabled DMGs
dmg:
internetEnabled: true # Default: falseInternet-enabled DMGs automatically extract their contents and eject after first open. This is an older pattern and is no longer recommended for modern apps.
DMG License
To display a license agreement when users open the DMG, set the license option in your DMG configuration or use the file-naming convention.
Configuration-based (recommended)
Specify the license file path directly in electron-builder.yml:
dmg:
license: "build/license.rtf"For multi-language builds, provide a language-code → file-path map:
dmg:
license:
en_US: "build/license.rtf"
de_DE: "build/license_de.txt"
fr_FR: "build/license_fr.txt"
ja_JP: "build/license_ja.txt"
zh_CN: "build/license_zh.txt"Language codes use the ll_CC format (ISO 639-1 language code + ISO 3166-1 country code, underscore-separated). The first entry in the map is the fallback shown when the user's language is not present.
Supported file formats: .rtf (recommended for rich text), .txt (plain text), .html.
:::tip[RTF recommended]
Use .rtf format for the best cross-language compatibility. Plain .txt files for Japanese, Korean, and Simplified/Traditional Chinese require macOS's CJK codec support in the bundled Python runtime.
:::
File-naming convention (automatic discovery)
As an alternative, place license files in your build resources directory using this naming pattern — electron-builder will pick them up automatically:
| Filename | Language |
|---|---|
license_en.txt or eula_en.txt |
English |
license_de.rtf |
German |
license_fr.html |
French |
license_ja.txt |
Japanese |
license_zh_CN.txt |
Simplified Chinese |
The language code is derived from the _LANG suffix. The user's macOS language preference determines which license is shown; en_US is the fallback.
:::note[One file per language]
Having both license_en.txt and eula_en.txt in the same build resources directory is an error — both map to en_US and electron-builder will throw an InvalidConfigurationError. Use the config-based license map if you need to control which file is used.
:::
Customizing License Button Labels
Override the default button text (Agree / Disagree / Print / Save) for each language by creating licenseButtons_LANG.json or licenseButtons_LANG.yml files in your build resources directory:
{
"languageName": "English",
"agree": "Accept",
"disagree": "Decline",
"print": "Print",
"save": "Save",
"message": "Please read the license agreement below."
}Name the file licenseButtons_en.json for English, licenseButtons_de.json for German, etc. The message field sets the prompt text at the top of the dialog. The legacy description field is also accepted as an alias for message.
Complete Example
dmg:
background: build/dmg-background.png
window:
position:
x: 130
y: 220
size:
width: 540
height: 380
contents:
- x: 130
y: 220
type: file
- x: 410
y: 220
type: link
path: /Applications
iconSize: 80
iconTextSize: 12
title: "${productName} ${version}"
format: UDZO
# Single-language license
license: "build/license.rtf"Multi-language example:
dmg:
background: build/dmg-background.png
license:
en_US: "build/license.rtf"
de_DE: "build/license_de.txt"
fr_FR: "build/license_fr.txt"
window:
size:
width: 540
height: 380
format: UDZOTroubleshooting
Icons appear in wrong positions: The x and y values in contents are absolute pixels from the top-left of the DMG window. Double-check that your coordinates are within the window.size bounds.
Background not appearing: Ensure the background image exactly matches the window.size dimensions. For Retina support, provide a @2x version at double the resolution.
DMG too large: The default UDZO format compresses well. If size is critical, try UDBZ for better (slower) compression.
License not appearing: If using the config option, verify the license path resolves relative to your build resources directory or project root. If using the file-naming convention, confirm the file is in the build resources directory with the correct pattern (license_en.txt, eula_en.txt, etc.) and a supported extension (.rtf, .txt, .html).
"Multiple license files found" error: You have both license_en.txt and eula_en.txt (or two files that resolve to the same language code) in your build resources. Remove the duplicate or use the config-based license map to point to the exact file you want.
CJK license not appearing: Japanese, Korean, and Chinese plain-text licenses require CJK codec support in the dmgbuild Python runtime. Use .rtf format for these languages, or ensure your dmgbuild bundle version is 1.2.5 or later.
Configuration
{!./app-builder-lib.Interface.DmgOptions.md!}
Donate (`website/docs/donate.md`)
title: Donate
electron-builder is free, open-source software driven through personal development time and community contributions.
This open-source work is done during free time. If you find it valuable, please consider donating!
Environment Variables (`website/docs/environment-variables.md`)
title: "Environment Variables"
Environment Variables
electron-builder reads a handful of environment variables — almost all of them secrets (signing certificates, publish tokens) or CI-provided values (the release tag, repository slug). Everything else is configured through your configuration file, not the environment.
This page is a grouped reference for every environment variable electron-builder itself reads. For the deeper "how", each group links to the relevant guide.
:::warning[Upgrading from v26?]
Several environment variables were removed in v27 — most notably CI_BUILD_TAG (use CI_COMMIT_TAG), ALLOW_ELECTRON_BUILDER_AS_PRODUCTION_DEPENDENCY, and every toolset-override variable (USE_SYSTEM_WINE, ELECTRON_BUILDER_NSIS_DIR, …). See Removed in v27 below and the v27 breaking changes.
:::
Code signing
Signing credentials are read from the environment so certificates and passwords never have to live in your config. Never commit them to source control — inject them via CI secrets. Each value accepts an HTTPS URL, a file:// path, a local path, or a base64-encoded certificate string.
Shared / macOS
| Variable | Description |
|---|---|
CSC_LINK |
The certificate (.p12/.pfx) — HTTPS URL, file:// path, local path, or base64 string. |
CSC_KEY_PASSWORD |
Password that decrypts the certificate at CSC_LINK. |
macOS installer (PKG)
| Variable | Description |
|---|---|
CSC_INSTALLER_LINK |
Certificate for signing PKG installers (a Developer ID Installer certificate). |
CSC_INSTALLER_KEY_PASSWORD |
Password for the installer certificate. |
Windows
| Variable | Description |
|---|---|
WIN_CSC_LINK |
Windows certificate. Falls back to CSC_LINK if unset. |
WIN_CSC_KEY_PASSWORD |
Password for the Windows certificate. Falls back to CSC_KEY_PASSWORD if unset. |
:::tip[Cross-compiling Windows on macOS]
Use WIN_CSC_LINK / WIN_CSC_KEY_PASSWORD to supply a separate Windows certificate while keeping your macOS certificate in CSC_LINK / CSC_KEY_PASSWORD.
:::
:::note[More signing variables]
The macOS keychain/identity variables (CSC_NAME, CSC_IDENTITY_AUTO_DISCOVERY, CSC_KEYCHAIN) and Azure Trusted Signing variables (AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET) are documented in the code-signing guides: Code Signing, macOS, and Windows. In v27, WIN_CSC_LINK fallback applies only to signtool mode; hsm, pkcs11, and azure are configured through win.sign.
:::
Notarization (macOS)
For notarizing macOS apps distributed outside the Mac App Store. Provide one of the three credential sets. Full setup is in the Notarization guide.
| Variable | Description |
|---|---|
APPLE_ID |
Apple ID email (Apple-ID auth). |
APPLE_APP_SPECIFIC_PASSWORD |
App-specific password for that Apple ID. |
APPLE_TEAM_ID |
Apple Developer Team ID. |
APPLE_API_KEY |
Path to the App Store Connect API key (.p8). |
APPLE_API_KEY_ID |
Key ID for the App Store Connect API key. |
APPLE_API_ISSUER |
Issuer ID for the App Store Connect API key. |
APPLE_KEYCHAIN |
Keychain to search for a stored notarization profile. |
APPLE_KEYCHAIN_PROFILE |
Name of a notarytool keychain profile to use. |
Publishing
Publish providers read their credentials from the environment. Publishing is never automatic — you must pass --publish explicitly (see Publish).
GitHub
| Variable | Description |
|---|---|
GH_TOKEN / GITHUB_TOKEN |
GitHub token. If either is defined, publishing defaults to [{ provider: "github" }]. |
GITHUB_RELEASE_TOKEN |
If defined, used instead of GH_TOKEN/GITHUB_TOKEN to publish the release (so you can keep a read-only token for update checks and a read-write token for releasing). |
GitLab
| Variable | Description |
|---|---|
GITLAB_TOKEN |
GitLab token. Required to publish to the gitlab provider. |
Bitbucket
| Variable | Description |
|---|---|
BITBUCKET_TOKEN |
Bitbucket token. Required to publish to the bitbucket provider. |
BITBUCKET_USERNAME |
Bitbucket username / Atlassian account email — selects the auth scheme (see below). |
:::warning[v27 auth change]
In v27 the Bitbucket publisher selects its auth scheme by whether a username is present:
BITBUCKET_USERNAMEset → HTTP Basic auth. Use this for an app password or an Atlassian API token.- No username → the token is sent as
Authorization: Bearer <token>(a repository / project / workspace access token).
If BITBUCKET_TOKEN holds an app password or API token, you must also set BITBUCKET_USERNAME. See the breaking-change note.
:::
Keygen
| Variable | Description |
|---|---|
KEYGEN_TOKEN |
Keygen token. If defined (and no GitHub token is), publishing defaults to [{ provider: "keygen" }]. |
Cloudflare R2
| Variable | Description |
|---|---|
CF_R2_ACCESS_KEY_ID |
R2 access key ID (an R2 API token with Object Read & Write). |
CF_R2_SECRET_ACCESS_KEY |
R2 secret access key. |
Amazon S3
| Variable | Description |
|---|---|
AWS_ACCESS_KEY_ID |
AWS access key ID. |
AWS_SECRET_ACCESS_KEY |
AWS secret access key. |
AWS_SESSION_TOKEN |
Optional session token for temporary credentials. |
AWS_PROFILE |
Named profile in ~/.aws/credentials to use when the keys above are not set (default: default). |
DigitalOcean Spaces
| Variable | Description |
|---|---|
DO_KEY_ID |
Spaces access key ID. |
DO_SECRET_KEY |
Spaces secret access key. |
Snap Store
| Variable | Description |
|---|---|
SNAP_CSC_LINK |
Base64-encoded Snap Store credentials or a file path (the CI-friendly equivalent of snapcraft.cscLink). Used for Snap Store publishing and Launchpad remote builds. |
SNAPCRAFT_STORE_CREDENTIALS |
Plain-text macaroon, read directly by snapcraft. |
See Snap → Authentication for the credential resolution order.
CI and release tags
electron-builder detects the release tag and repository from standard CI variables. These are normally set by your CI provider, not by you — the exception is CI_COMMIT_TAG, which you can set manually (e.g. on GitLab or self-hosted CI) to provide the tag.
| Variable | Description |
|---|---|
CI_COMMIT_TAG |
The release tag (GitLab's standard variable). Set this manually where no other tag variable is present. |
TRAVIS_TAG |
Tag on Travis CI. |
APPVEYOR_REPO_TAG_NAME |
Tag on AppVeyor. |
CIRCLE_TAG |
Tag on CircleCI. |
BITRISE_GIT_TAG |
Tag on Bitrise. |
BITBUCKET_TAG |
Tag on Bitbucket Pipelines. |
GITHUB_REF_TYPE / GITHUB_REF_NAME |
On GitHub Actions, the tag is taken from GITHUB_REF_NAME when GITHUB_REF_TYPE is tag. |
The GitHub repository is auto-detected from TRAVIS_REPO_SLUG, APPVEYOR_REPO_NAME, or CIRCLE_PROJECT_USERNAME/CIRCLE_PROJECT_REPONAME (falling back to your package.json repository field or .git/config).
:::warning[CI_BUILD_TAG removed in v27]CI_BUILD_TAG was removed — use CI_COMMIT_TAG instead. See the breaking-change note.
:::
Cache & download
| Variable | Description |
|---|---|
ELECTRON_BUILDER_CACHE |
Overrides the cache directory used for downloaded Electron binaries and toolsets. Must be an absolute path. Defaults to ~/Library/Caches/electron-builder (macOS), %LOCALAPPDATA% (Windows), or $XDG_CACHE_HOME / ~/.cache (Linux). |
USE_HARD_LINKS |
Set to "true" to copy files via hard links where possible (faster, less disk use). Ignored on Windows. |
:::note[Electron mirror is config, not an env var]
To download Electron from a mirror, configure electronGet.mirrorOptions in your build config — e.g. electronGet: { mirrorOptions: { mirror: "https://my-mirror/" } }. (In v26 this was electronDownload.mirror.) See Configuration.
:::
Debugging
| Variable | Description |
|---|---|
DEBUG=electron-builder |
Enables verbose debug logging via the debug package. |
DEBUG=electron-builder electron-builder --linux snapNamespaced channels are also available — e.g. DEBUG=electron-builder:7z for archive operations, or DEBUG=electron-builder* for everything.
Removed in v27
These variables were removed in v27. electron-builder migrate-schema does not rewrite them — update your scripts and config manually.
| Removed variable | Replacement |
|---|---|
CI_BUILD_TAG |
CI_COMMIT_TAG |
ALLOW_ELECTRON_BUILDER_AS_PRODUCTION_DEPENDENCY |
The ignoredProductionDependencies build option — see the breaking-change note. |
Toolset overrides
The environment variables that pointed electron-builder at custom build-tool bundles are all removed. Supply a custom bundle through a ToolsetCustom object on the relevant toolsets key instead (toolsets.<name>: { url, checksum }).
| Removed variable | What it controlled |
|---|---|
USE_SYSTEM_WINE |
Forced host-installed Wine instead of the bundle. Replaced by toolsets.wine: "system". |
USE_SYSTEM_SIGNCODE |
Forced host signtool/signcode. |
USE_SYSTEM_OSSLSIGNCODE |
Forced host osslsigncode. |
USE_SYSTEM_FPM |
Forced host-installed fpm. |
APPIMAGE_TOOLS_PATH |
AppImage build tools (mksquashfs, runtime). |
LINUX_TOOLS_MAC_PATH |
Linux-tools-mac bundle (ar, lzip, gtar). |
CUSTOM_FPM_PATH |
FPM executable. |
ELECTRON_BUILDER_NSIS_DIR |
NSIS compiler bundle directory. |
ELECTRON_BUILDER_NSIS_RESOURCES_DIR |
NSIS resources/plugins directory. |
CUSTOM_NSIS_RESOURCES |
Alternate NSIS resources bundle. |
ELECTRON_BUILDER_WINE_TOOLSET_DIR |
Wine bundle directory. |
The two signing USE_SYSTEM_* variables (USE_SYSTEM_SIGNCODE, USE_SYSTEM_OSSLSIGNCODE) have no env-var replacement — configure signing through win.sign and the winCodeSign toolset. For the full replacement mechanics (archive formats, checksums, file:// directories), see Toolset env-var overrides removed.
File Patterns (`website/docs/file-patterns.md`)
title: "File Patterns"
*Matches 0 or more characters in a single path portion?Matches 1 character[...]Matches a range of characters, similar to a RegExp range.
If the first character of the range is!or^then it matches
any character not in the range.!(pattern|pattern|pattern)Matches anything that does not match
any of the patterns provided.?(pattern|pattern|pattern)Matches zero or one occurrence of the
patterns provided.+(pattern|pattern|pattern)Matches one or more occurrences of the
patterns provided.*(a|b|c)Matches zero or more occurrences of the patterns provided@(pattern|pat*|pat?erN)Matches exactly one of the patterns
provided**If a "globstar" is alone in a path portion, then it matches
zero or more directories and subdirectories searching for matches.
It does not crawl symlinked directories.
If directory matched, all contents are copied. So, you can just specify foo to copy foo directory.
:::tip[Excluding directories]
Remember that !doNotCopyMe/**/* would match the files in the doNotCopyMe directory, but not the directory itself, so the empty directory would be created.
Solution — use macro ${/*}, e.g. !doNotCopyMe${/*}.
:::
Multiple Glob Patterns
[
// match all files
"**/*",
// except for js files in the foo/ directory
"!foo/*.js",
// unless it's foo/bar.js
"foo/bar.js",
]Default Ignored Files
electron-builder excludes a set of files from the packaged app by default (development files, and certain extensions/names such as Wavefront .obj 3D models). In v27 the disableDefaultIgnoredFiles option was removed. To keep a file that is excluded by default, add an explicit files glob that names it concretely — an explicit include now overrides the matching default exclusion:
{
"build": {
"files": [
"**/*",
"**/*.obj" // opt a default-excluded extension back in
]
}
}Broad patterns such as **/* still honor the defaults; only a pattern that names the extension or directory concretely opts it back in. See v27 Breaking Changes → disableDefaultIgnoredFiles.
File Macros
You can use macros in the file patterns, artifact file name patterns and publish configuration url:
${arch}— expanded toia32,x64. If noarch, macro will be removed from your pattern with leading space,-and_(so, you don't need to worry and can reuse pattern).${os}— expanded tomac,linuxorwinaccording to target platform.${platform}— expanded todarwin,linuxorwin32according to Node.jsprocess.platformproperty.${name}–package.jsonname.${productName}— Sanitized product name.${version}${channel}— detected prerelease component from version (e.g.beta).${env.ENV_NAME}— any environment variable.- Any property of AppInfo (e.g.
buildVersion,buildNumber).
Flatpak (`website/docs/flatpak.md`)
:::warning[Single-file Flatpak bundles]
Currently electron-builder does not support publishing apps to Flatpak repositories like Flathub. Flatpak support in electron-builder is limited to generating single-file bundles which have limitations compared to apps installed from a repository.
:::
The top-level flatpak key contains a set of options instructing electron-builder on how it should build a Flatpak bundle.
What is Flatpak?
Flatpak is a universal Linux packaging system that runs apps in a sandbox with controlled access to system resources. Unlike AppImage (no sandbox) and Snap (Ubuntu-primary), Flatpak is designed to be distro-agnostic and runs on any Linux distribution with flatpak installed.
Key characteristics:
- Sandboxed — apps run in an isolated environment with portal-based access to system resources
- Cross-distro — works on Ubuntu, Fedora, Arch, Debian, and more
- Runtime-based — apps are built against a specific runtime (freedesktop, GNOME, KDE)
- Distributed via Flathub (for repository-based distribution — note: currently not supported by electron-builder)
Flatpak vs. AppImage vs. Snap
| Aspect | Flatpak | AppImage | Snap |
|---|---|---|---|
| Sandboxed | Yes (portals) | No | Yes (interfaces) |
| Distro support | Any (flatpak required) | Universal | Any (snapd required) |
| Store/repo | Flathub (not via eb) | N/A | Snap Store |
| Auto-update | Flathub (not via eb) | AppImageUpdate | Snap Store |
| electron-builder output | Single-file bundle | Single file | Package |
Build Requirements
Install flatpak and flatpak-builder on your Linux build machine — see Flatpak setup.
Runtime and SDK
Flatpak apps run against a runtime that provides base libraries. Choose the runtime that matches your app's dependencies:
flatpak:
runtime: org.freedesktop.Platform # freedesktop (most common for Electron)
sdk: org.freedesktop.Sdk
runtimeVersion: "24.08" # runtime versionCommon runtimes for Electron apps:
| Runtime | SDK | Use Case |
|---|---|---|
org.freedesktop.Platform |
org.freedesktop.Sdk |
Standard Electron apps (recommended) |
org.gnome.Platform |
org.gnome.Sdk |
GNOME-integrated apps |
org.kde.Platform |
org.kde.Sdk |
KDE-integrated apps |
Install the runtime on your build machine before building — see available Flatpak runtimes.
Finish Args (Permissions)
The finishArgs array defines what the sandboxed app is allowed to access. Electron apps need several permissions:
flatpak:
finishArgs:
# Display
- "--socket=x11"
- "--socket=wayland"
- "--share=ipc" # shared memory with X11 compositor
# Network
- "--share=network"
# Filesystem
- "--filesystem=home" # access user home directory
# Desktop integration
- "--talk-name=org.freedesktop.Notifications"
- "--talk-name=org.kde.StatusNotifierWatcher"
- "--talk-name=com.canonical.AppMenu.Registrar"
# Electron-specific
- "--device=dri" # GPU access
- "--socket=pulseaudio" # audioCommon finish args for Electron:
| Arg | Purpose |
|---|---|
--socket=x11 |
X11 display |
--socket=wayland |
Wayland display |
--share=ipc |
Shared memory (required for X11) |
--share=network |
Network access |
--filesystem=home |
Home directory access |
--device=dri |
GPU/DRM device (for WebGL, video decode) |
--socket=pulseaudio |
Audio output |
--talk-name=org.freedesktop.Notifications |
Desktop notifications |
--filesystem=xdg-download |
Downloads folder only (less than --filesystem=home) |
Branch
flatpak:
branch: stable # Default: master/mainThe branch name is embedded in the Flatpak bundle and visible to users.
Extra Modules
Include additional system libraries not in the runtime:
flatpak:
modules:
- name: zlib
buildsystem: autotools
sources:
- type: archive
url: https://www.zlib.net/zlib-1.3.tar.gz
sha256: "..."Custom Files and Symlinks
Copy additional files into the app bundle:
flatpak:
files:
- - build/extra-binary # source
- /app/bin/extra-binary # destination (absolute within Flatpak)
symlinks:
- - /app/bin/myapp # target
- /app/bin/myapp-alias # symlink locationWayland Support
flatpak:
useWaylandFlags: true # Appends --enable-features=UseOzonePlatform --ozone-platform=wayland when running under a Wayland sessionLocal Testing
Build and install the Flatpak locally:
electron-builder --linux flatpak
# This produces a .flatpak bundle file
# Install it (no root required)
flatpak install --user dist/myapp-1.0.0.flatpak
# Run it
flatpak run com.mycompany.myappTroubleshooting
If the build fails, enable debug logging to see the underlying flatpak-builder output:
env DEBUG="@malept/flatpak-bundler" electron-builder build --linux flatpakMissing runtime: Install the runtime before building — see the Runtime section above.
Sandbox permission denied: Check the finishArgs — the app is likely trying to access something not in its allowed permissions. Run flatpak run --log-session-bus com.mycompany.myapp to see D-Bus access denials.
GPU rendering issues: Ensure --device=dri is in finishArgs for WebGL support.
Configuration
{!./app-builder-lib.Interface.FlatpakOptions.md!}
Github Actions (`website/docs/github-actions.md`)
GitHub Actions CI/CD
This guide shows how to build, sign, and publish Electron apps using GitHub Actions for all three platforms (macOS, Windows, Linux).
Basic Concepts
- Secrets — store certificates, passwords, and API tokens in GitHub repository secrets (Settings → Secrets and variables → Actions)
- Matrix builds — build for multiple platforms in parallel using a matrix strategy
GH_TOKEN— the GitHub token electron-builder uses to upload release assets. UseGITHUB_TOKEN(provided automatically) or a PAT withreposcope
Minimal Multi-Platform Workflow
This workflow builds for macOS, Windows, and Linux on every push to main and on version tags:
name: Build and Release
on:
push:
branches: [main]
tags: ['v*']
jobs:
build:
strategy:
matrix:
os: [macos-latest, windows-latest, ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Build
run: npx electron-builder --publish never
env:
# macOS signing
CSC_LINK: ${{ secrets.MAC_CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.MAC_CSC_KEY_PASSWORD }}
# Windows signing
WIN_CSC_LINK: ${{ secrets.WIN_CSC_LINK }}
WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: dist-${{ matrix.os }}
path: dist/Publish on Tag Push
To publish GitHub releases automatically when you push a version tag (v1.2.3):
name: Release
on:
push:
tags: ['v*']
jobs:
release:
strategy:
matrix:
os: [macos-latest, windows-latest, ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Publish
run: npx electron-builder --publish always
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# macOS signing + notarization
CSC_LINK: ${{ secrets.MAC_CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.MAC_CSC_KEY_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
# Windows signing
WIN_CSC_LINK: ${{ secrets.WIN_CSC_LINK }}
WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}The --publish always flag uploads artifacts to the GitHub release for the pushed tag. If no release exists, electron-builder creates a draft release.
Draft Release Workflow
A common pattern is to publish a draft release on every tagged push, then manually promote it to a full release after reviewing:
# In electron-builder.yml
publish:
provider: github
releaseType: draftThen in GitHub: go to Releases → find the draft → click "Edit" → "Publish release".
macOS Code Signing Setup
Preparing the Certificate
# Export your Developer ID Application certificate from Keychain Access
# File → Export Items → save as .p12 with a password
# Encode to base64
base64 -i certificate.p12 | pbcopy # copies to clipboardAdd to GitHub Secrets:
MAC_CSC_LINK— the base64-encoded.p12contentMAC_CSC_KEY_PASSWORD— the certificate password
macOS Signing
Pass CSC_LINK and CSC_KEY_PASSWORD directly — electron-builder creates and manages a temporary keychain automatically:
build-mac:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Build and notarize
run: npx electron-builder --mac --publish always
env:
CSC_LINK: ${{ secrets.MAC_CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.MAC_CSC_KEY_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}macOS Secrets Reference
| Secret | How to Get |
|---|---|
MAC_CSC_LINK |
base64 -i certificate.p12 |
MAC_CSC_KEY_PASSWORD |
The password you set when exporting the .p12 |
APPLE_ID |
Your Apple ID email (developer account) |
APPLE_APP_SPECIFIC_PASSWORD |
Create at appleid.apple.com → App-Specific Passwords |
APPLE_TEAM_ID |
10-character team ID from developer.apple.com/account |
Windows Code Signing Setup
# Encode your Windows certificate (.pfx) to base64
# PowerShell:
[Convert]::ToBase64String([IO.File]::ReadAllBytes("certificate.pfx")) | Out-File cert_encoded.txt
# bash (Linux/macOS):
base64 certificate.pfx > cert_encoded.txtAdd to GitHub Secrets:
WIN_CSC_LINK— base64-encoded.pfxcontentWIN_CSC_KEY_PASSWORD— certificate password
Using Azure Trusted Signing
- name: Build and sign (Azure)
run: npx electron-builder --win --publish always
env:
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}Configure win.sign with type: "azure" in your electron-builder config. See Windows Code Signing.
Caching for Faster Builds
Cache npm packages and Electron binaries to avoid redundant downloads:
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm # caches ~/.npm
- name: Cache Electron
uses: actions/cache@v4
with:
path: |
~/.cache/electron
~/.cache/electron-builder
key: ${{ runner.os }}-electron-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-electron-Linux Snapcraft Publishing
To publish to the Snap Store, add a SNAPCRAFT_STORE_CREDENTIALS secret (obtained by running snapcraft export-login -):
- name: Build and publish Snap
run: npx electron-builder --linux snap --publish always
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}Complete Production Workflow
A production-grade workflow with separate jobs per platform, caching, and GitHub release publishing:
/* Detailed source-code truncated for AI context efficiency. */Troubleshooting
"Resource not accessible by integration"
: The workflow doesn't have permission to create releases. Add permissions: contents: write to the job or workflow. Also ensure GH_TOKEN is set.
macOS: "No identity found"
: CSC_LINK or CSC_KEY_PASSWORD is wrong or missing. Verify the base64-encoded certificate decodes to a valid .p12 and that the password is correct. Run echo "$CSC_LINK" | base64 --decode | openssl pkcs12 -info -passin pass:"$CSC_KEY_PASSWORD" locally to verify.
Windows: certificate base64 too long
: Windows CI environments may truncate environment variables over 8192 characters. Re-export the .pfx without the full certificate chain included. See Code Signing.
Build fails with "Cannot find module"
: Native modules need to be rebuilt for the target Electron version. Ensure npm ci (not npm install) is used, and check that electron-rebuild or @electron/rebuild is configured if needed.
Related Pages
- Code Signing — environment variables, certificate types
- macOS Notarization — notarization setup and troubleshooting
- Auto Update — configuring electron-updater with GitHub releases
- Multi Platform Build — building for other platforms using Docker
Glossary (`website/docs/glossary.md`)
Glossary
Quick reference for terms used throughout the electron-builder documentation.
A
Ad-hoc signing
: A macOS code signature that identifies the binary but isn't tied to an Apple developer identity. Used for local development on Apple Silicon. Set mac.sign.identity to "-" to request ad-hoc signing. Unlike a proper Developer ID signature, ad-hoc signatures are not trusted by Gatekeeper for distribution.
AppImage
: A self-contained Linux application format. A single executable file that runs on any Linux distribution without installation. Users download the file, mark it executable (chmod +x), and run it. One of the two default Linux targets for electron-builder (along with Snap). See AppImage.
AppX / MSIX
: Microsoft's modern app packaging format for the Windows Store and enterprise MDM deployment. electron-builder's appx target produces .appx files. Microsoft's newer .msix format is functionally similar. See AppX.
ASAR
: Atom Shell ARchive — Electron's archive format for bundling application source files. Similar to a tar file but optimized for random access. Files inside ASAR are read through Electron's virtual filesystem. Native modules (.node files) must be excluded via asar.unpack.
asar.unpack
: A configuration option listing glob patterns for files that should be placed in app.asar.unpacked/ rather than inside the ASAR archive. Required for native modules and large binaries that need direct filesystem access.
C
CA (Certificate Authority)
: An organization that issues and vouches for digital certificates. For Windows code signing, any major CA (DigiCert, Sectigo, SSL.com) works. For macOS, only Apple-issued certificates are recognized by Gatekeeper.
CFBundleIdentifier
: The unique reverse-DNS identifier for a macOS application bundle (e.g., com.company.myapp). Set via appId in electron-builder config. Must be unique across all apps on a user's machine and within the App Store.
CSC (Code Signing Certificate)
: The prefix for environment variables that provide code signing credentials to electron-builder (CSC_LINK, CSC_KEY_PASSWORD, etc.). See Code Signing.
D
DMG
: Disk iMaGe — the standard macOS distribution format. A virtual disk that mounts when double-clicked, showing the app icon and an Applications shortcut for drag-and-drop installation. See DMG.
Developer ID
: An Apple code signing certificate type for apps distributed outside the Mac App Store. Comes in two forms: "Developer ID Application" (for signing app bundles) and "Developer ID Installer" (for signing PKG installers). Both require Apple Developer Program membership.
E
Entitlements
: macOS security declarations that grant your app specific capabilities. Required when Hardened Runtime is enabled. Stored in .plist XML files referenced by mac.sign.entitlements and mac.sign.entitlementsInherit. Examples: com.apple.security.cs.allow-jit (required by Electron), com.apple.security.network.client (outbound networking).
EV Certificate (Extended Validation)
: A Windows code signing certificate with higher trust than standard OV certificates. EV certificates are physically bound to a USB security key and cannot be exported — which makes them incompatible with most CI/CD systems. Since 2024, SmartScreen no longer treats EV-signed installers differently from OV-signed ones; both build reputation over time.
F
Flatpak
: A Linux packaging system that runs apps in a sandbox with controlled system access. Apps declare required permissions via "finish args." electron-builder produces single-file Flatpak bundles (not Flathub repository packages). See Flatpak.
FPM (Effing Package Management)
: A command-line tool used internally by electron-builder to create DEB, RPM, Pacman, APK, FreeBSD, and P5P packages. You generally don't interact with it directly; use fpm passthrough options in your config for advanced cases.
Fuses
: Feature flags embedded in the Electron binary that control security-sensitive behaviors at build time. Unlike runtime flags, fuses cannot be changed after the app is packaged. See Adding Electron Fuses.
G
Gatekeeper
: Apple's security system that checks apps for valid code signatures and (on macOS 10.15+) notarization before allowing them to run. An app without a valid Developer ID signature prompts a security warning or is blocked entirely.
GUID (Globally Unique Identifier)
: A 128-bit identifier used by Windows installers to track application identity across installs and upgrades. In NSIS, this is the guid option. In MSI, this is the upgradeCode. Critical: changing the GUID between releases breaks silent upgrades of existing installations.
H
Hardened Runtime
: A macOS security mode (mac.sign.hardenedRuntime: true) that restricts what your app can do without explicit entitlement declarations. Required for notarization. It blocks JIT compilation, unsigned library loading, and other potentially dangerous operations unless granted via entitlements.
I
IDP (Installer Data Package / P5P)
: Solaris IPS (Image Packaging System) package format. electron-builder's p5p target produces packages for Solaris and illumos-based distributions.
K
Keychain
: macOS's system for storing credentials, certificates, and keys. electron-builder reads code signing certificates from the keychain. On CI, you create a temporary keychain, import the certificate, and unlock it before building.
M
MAS (Mac App Store)
: Apple's app distribution platform for macOS. MAS apps are sandboxed and require entitlements for any system access. electron-builder's mas target builds for App Store submission; mas-dev builds for local testing with a development provisioning profile. See MAS.
N
Notarization
: Apple's service that scans your app for malware and issues a cryptographic ticket confirming the scan. Required for distributing apps outside the Mac App Store on macOS 10.15+. electron-builder handles notarization automatically when mac.notarize: true. See Notarization.
NSIS (Nullsoft Scriptable Install System)
: The Windows installer framework used by electron-builder's nsis and nsis-web targets. Produces highly customizable .exe installers. The default Windows target. See NSIS.
O
OV Certificate (Organization Validated)
: A standard Windows code signing certificate that requires organizational identity verification. Unlike EV certificates, OV certificates can be exported as .pfx files and used in CI/CD. SmartScreen trust builds gradually based on how many users have downloaded and run the installer.
P
PKG
: macOS installer package format. Used when your app needs to install files outside /Applications, run pre/post-install scripts, or install system components like launch daemons. Requires a separate "Developer ID Installer" certificate. See PKG.
Portable
: A Windows build target that produces a single .exe file requiring no installation. The app runs directly without writing to the registry or creating Start menu entries. See the Windows targets section in NSIS.
Provisioning Profile
: A macOS/iOS file that ties a certificate, an App ID, and a set of entitlements together. Required for Mac App Store submissions (mas target). Obtained from the Apple Developer portal.
S
Sandbox
: An isolation mechanism that restricts what an application can access. Flatpak and Snap use sandboxes on Linux. Mac App Store apps are sandboxed by Apple's App Sandbox framework. Sandboxed apps must declare all required permissions.
Signing Identity
: The name of a code signing certificate as it appears in the macOS Keychain (e.g., Developer ID Application: My Company (ABCDE12345)). Referenced by CSC_NAME or mac.sign.identity in electron-builder config.
SmartScreen
: Windows Defender's application reputation service. It warns users when they run unsigned apps or apps from publishers with low download reputation. Since 2024, no certificate type bypasses the reputation-building period; EV and OV certificates both require time.
Snap
: Canonical's Linux packaging format, primarily for Ubuntu. Snap apps run in a sandbox with declared "interface plugs" controlling system access. electron-builder's snap target produces Snap packages for Snap Store distribution. See Snap.
Squirrel
: A Windows auto-update framework. electron-builder's squirrel.windows target is a legacy option — new projects should use NSIS with electron-updater instead. See Squirrel.Windows.
Stapling
: Attaching a notarization ticket to an app bundle so that Gatekeeper can verify notarization offline (without contacting Apple's servers). electron-builder staples the ticket automatically after notarization completes.
U
Universal Binary
: A macOS binary that contains both x64 (Intel) and arm64 (Apple Silicon) code. The OS selects the appropriate slice at launch. Build with --universal or arch: universal. Results in a ~2× larger download but runs natively on all modern Macs.
UpgradeCode
: A GUID in MSI packages that identifies the application across versions for upgrade and uninstall operations. Must remain constant across all versions. Changing the UpgradeCode breaks silent upgrades and leaves old versions orphaned on managed systems. See MSI.
W
WiX (Windows Installer XML Toolset)
: The toolchain used by electron-builder internally to produce MSI packages. Provides advanced Windows installer features like component tracking, repair, and rollback. You can pass additional WiX arguments via msi.additionalWixArgs.
Linux (`website/docs/linux.md`)
title: "Linux"
{!./partials/_upgrading-from-v26.md!}
The top-level linux key contains a set of options instructing electron-builder on how it should build Linux targets. These options are applicable to any Linux target.
Linux Target Overview
electron-builder supports a wide range of Linux package formats. Choose based on your target audience and distribution method:
| Target | Format | Best For | Distro Support |
|---|---|---|---|
AppImage |
Single file | Broad compatibility, no install | Universal |
deb |
Debian package | Ubuntu, Debian, Mint | Debian-based |
rpm |
RPM package | Fedora, RHEL, openSUSE | Red Hat-based |
snap |
Snap package | Ubuntu-primary | Any (snapd required) |
flatpak |
Flatpak | Cross-distro sandboxed | Any (flatpak required) |
pacman |
Arch package | Arch Linux, Manjaro | Arch-based |
apk |
Alpine package | Alpine Linux, containers | Alpine |
freebsd |
FreeBSD pkg | FreeBSD | FreeBSD |
p5p |
Solaris IPS | Solaris, illumos | Solaris-based |
zip, 7z, tar.* |
Archive | Custom CDN distribution | Universal |
dir |
Directory | Development/debugging | N/A |
The default targets are AppImage and Snap.
Common Linux Configuration
Options set in the linux key apply to all Linux targets:
linux:
target:
- AppImage
- deb
- rpm
category: Utility # freedesktop.org category
synopsis: "A short description" # one-line description
description: "Full description" # longer description
maintainer: "Name <email>" # package maintainer
vendor: "My Company" # package vendor
mimeTypes:
- application/x-myapp
executableArgs:
- --enable-features=WebContentsFocusOnResize:::note[v27: launcher entrypoint]
Every Linux target (deb/rpm, AppImage, snap, flatpak) now launches through a generated <executableName>-launcher shell script rather than invoking the executable directly. executableArgs (and forceX11) are injected into that launcher, and the generated .desktop Exec key points at it (e.g. Exec=/opt/MyApp/MyApp-launcher %U) rather than inlining the args. This makes executableArgs apply consistently across all Linux targets. If you ship a custom .desktop override, an AppArmor/snap profile, or a MIME handler that hard-codes the Exec command, point it at the *-launcher script. See v27 Breaking Changes → Linux launcher entrypoint.
:::
Desktop File Customization
All Linux targets include a .desktop file for application menu integration. Customize it via linux.desktop:
linux:
desktop:
entry:
Name: My Application
Comment: A fantastic application
Categories: Office;Productivity
Keywords: productivity;notes;organizer
StartupWMClass: my-app # helps window managers match window to icon
Terminal: false
Type: Application
desktopActions:
NewWindow:
Name: New Window
Exec: my-app --new-window
NewFile:
Name: New File
Exec: my-app --new-fileStandard freedesktop.org category values include: AudioVideo, Audio, Video, Development, Education, Game, Graphics, Network, Office, Science, Settings, System, Utility.
Window Association (`desktopName`)
Electron derives its app_id — which becomes the window's WM_CLASS — from the desktopName field in your root package.json. Desktop environments (GNOME, KDE, etc.) use WM_CLASS to match a running window to its .desktop entry for taskbar grouping, dock badges, and launcher highlighting.
Without desktopName set, this association can break: the fallback WM_CLASS may not match the installed .desktop filename, so the DE treats the running app as an unknown window.
To fix this, set desktopName in package.json:
{
"desktopName": "com.example.MyApp"
}electron-builder installs the .desktop file as ${desktopName}.desktop (here com.example.MyApp.desktop) and sets StartupWMClass=com.example.MyApp, both matching Electron's app_id. A trailing .desktop in desktopName is tolerated (stripped before the filename is composed), so configs written for earlier versions keep working. When desktopName is absent, the filename falls back to executableName.
:::note[Changed in v27]
The installed .desktop filename is now always derived from desktopName. The linux.syncDesktopName flag that previously gated this behaviour has been removed — see the v27 breaking changes.
:::
:::warning[desktopName is required for reliable window association]
If desktopName is absent, electron-builder will log a warning at build time. Without it, desktop environments on GNOME, KDE, and others may not correctly link the running application window to its launcher entry.
:::
See the freedesktop.org specification for all valid keys.
Icon Configuration
Provide a directory of PNG icons named with their size:
linux:
icon: build/icons/Files must be named NxN.png (e.g., 256x256.png, 512x512.png). If not specified, electron-builder auto-generates Linux icons from the macOS .icns file (if present) or the Windows .ico file.
Recommended sizes: 16, 24, 32, 48, 64, 96, 128, 256, 512 pixels.
Debian Package (`deb`)
The deb target creates a .deb package installable via apt on Debian, Ubuntu, Mint, and other Debian-based distributions.
deb:
depends:
- libgtk-3-0
- libnotify4
- libnss3
- libxss1
- libxtst6
- xdg-utils
- libatspi2.0-0
- libuuid1
- libsecret-1-0
recommends:
- libappindicator3-1 # system tray support
packageCategory: utils
priority: optionalCommon depends packages for Electron apps:
libgtk-3-0— GTK3 (required)libnotify4— desktop notificationslibnss3— NSS (Chromium networking)libxss1— screen saver extensionlibxtst6— X test extensionxdg-utils— desktop integration utilitieslibatspi2.0-0— accessibilitylibuuid1— UUID supportlibsecret-1-0— keychain/Secret Service
Priority values: required, important, standard, optional (use optional for most apps)
RPM Package (`rpm`)
The rpm target creates a .rpm package for Fedora, RHEL, CentOS, openSUSE, and other RPM-based distributions.
rpm:
depends:
- libXScrnSaver
- libnotify
- libappindicator
- nss
- gtk3
compression: xz # gz | bzip2 | xz | lzo (default: xz)
packageCategory: Applications/InternetCommon depends for Electron on RPM systems:
gtk3— GTK3libnotify— notificationslibXScrnSaver— screen savernss— networking
After-install/after-remove scripts:
rpm:
afterInstall: build/scripts/after-install.sh
afterRemove: build/scripts/after-remove.sh:::note[v27: maintainer-script variable syntax]
electron-builder interpolates variables into maintainer scripts (afterInstall / afterRemove) using shell-style ${var}. The legacy EJS <%= var %> interpolation was removed in v27 — update any templated maintainer scripts to ${var}. See v27 Breaking Changes → Linux maintainer-script EJS syntax.
:::
Pacman Package (`pacman`)
:::warning[Beta]
The pacman target is in beta. Test thoroughly before distributing.
:::
Creates a .pkg.tar.zst package for Arch Linux and Arch-based distributions (Manjaro, EndeavourOS, etc.):
pacman:
depends:
- gtk3
- libnotify
- nss
- libxss
- libxtst
- xdg-utils
- at-spi2-core
- libsecretAlpine APK Package (`apk`)
Creates an .apk package for Alpine Linux, commonly used in Docker containers:
apk:
depends:
- gtk+3.0
- libnotify
- nss:::note[musl libc]
Alpine uses musl libc rather than glibc. Electron bundles its own copy of glibc-dependent libraries, but some native modules may not be compatible with Alpine without recompilation.
:::
FreeBSD Package (`freebsd`)
Creates a .pkg package (not to be confused with macOS PKG) for FreeBSD systems:
freebsd:
depends:
- gtk3
- libnotify
- nssFPM Passthrough Options
The DEB, RPM, Pacman, APK, FreeBSD, and P5P targets are built using FPM internally. Pass additional FPM command-line arguments:
deb:
fpm:
- "--deb-changelog=CHANGELOG.md"
- "--deb-no-default-config-files"AppArmor Profile (Ubuntu 24+)
Ubuntu 24.04 introduced mandatory AppArmor profiles for application sandboxing. Specify a custom profile:
deb:
appArmorProfile: build/apparmor-profileWithout a profile, the app may be restricted by Ubuntu's default profile. See the AppArmor documentation for profile syntax.
Base Linux Configuration
{!./app-builder-lib.Interface.LinuxConfiguration.md!}
Debian Package Options
The top-level deb key contains a set of options for Debian packages.
{!./app-builder-lib.Interface.DebOptions.md!}
All LinuxTargetSpecificOptions can also be specified in deb to customize Debian packages.
`LinuxTargetSpecificOptions` — APK, FreeBSD, Pacman, P5P and RPM Options {#linuxtargetspecificoptions-apk-freebsd-pacman-p5p-and-rpm-options}
The top-level apk, freebsd, pacman, p5p and rpm keys contain options for their respective Linux targets.
{!./app-builder-lib.Interface.LinuxTargetSpecificOptions.md!}
Mac (`website/docs/mac.md`)
title: "macOS"
{!./partials/_upgrading-from-v26.md!}
The top-level mac key contains a set of options instructing electron-builder on how it should build macOS targets. These options are applicable to any macOS target.
macOS Target Overview
electron-builder supports several macOS distribution formats. Choose based on your distribution channel:
| Target | Best For | Signed? | Notarized? |
|---|---|---|---|
dmg |
Standard consumer distribution | Yes | Yes |
zip |
Update servers (electron-updater), minimal package | Yes | Yes |
pkg |
System-level installs, kernel extensions, launch daemons | Yes | Yes |
mas |
Mac App Store distribution | Yes (Mac App Distribution) | No (MAS handles it) |
mas-dev |
Local testing of MAS builds | Yes (Apple Development / Mac Developer) | No |
7z, tar.* |
Archive formats, custom CDN distribution | Optional | Optional |
dir |
Development/debugging — unpacked app | No | No |
The default targets are zip and dmg (both are required for Squirrel.Mac auto-update).
Bundle ID
The appId property sets the CFBundleIdentifier for your macOS app. This is a critical identifier — set it explicitly:
appId: "com.mycompany.myapp"- Use reverse-DNS format:
com.yourcompany.appname - Must be unique in the Mac App Store if you intend to submit there
- Changing it after first release will break existing user data paths (NSUserDefaults, sandboxed containers, etc.)
Architecture Support
electron-builder supports building for multiple CPU architectures:
| Architecture | CLI Flag | Description |
|---|---|---|
x64 |
--x64 |
Intel 64-bit (traditional Mac) |
arm64 |
--arm64 |
Apple Silicon (M1, M2, M3, M4) |
universal |
--universal |
Fat binary containing both x64 and arm64 |
Universal Binaries
A universal binary runs natively on both Intel and Apple Silicon Macs with no performance penalty:
mac:
target:
- target: dmg
arch: universalUniversal binary options:
mergeASARs— merge x64 and arm64 ASAR archives into a single universal ASAR (trueby default). Disable only if you have architecture-specific native modules that cannot be fat-binary merged.singleArchFiles— glob pattern for files that are single-arch and should NOT be merged (e.g., pre-built native binaries distributed only for one arch).x64ArchFiles— glob pattern for files that are x64-only. These are kept as x64 in the universal binary rather than being fat-binary merged.
In v27 these options live under mac.universal (a pass-through to @electron/universal) and only take effect when the target arch is universal:
mac:
universal:
mergeASARs: true
singleArchFiles: "**/*.node" # keep native modules as separate arch filesRecommended: Build Per-Arch on Correct Hardware
While cross-compilation is possible, the most reliable approach is to build arm64 on Apple Silicon and x64 on Intel (or use a matrix in CI). Universal builds work best when both arches are produced natively and then merged.
Code Signing
macOS apps must be signed to avoid Gatekeeper warnings. See Code Signing for full setup.
:::note[v27: signing options moved under mac.sign]
In v27 every macOS signing option (identity, entitlements, hardenedRuntime, type, provisioningProfile, binaries, requirements, timestamp, …) lives inside a single mac.sign object (sign.identity, sign.hardenedRuntime, …); signIgnore is now sign.ignore. electron-builder migrate-schema rewrites the old flat keys automatically. The same structure applies to mas and masDev. See v27 Breaking Changes → macOS signing.
:::
Certificate Identity
Use mac.sign.identity to specify the signing certificate by name:
mac:
sign:
identity: "Developer ID Application: My Company (TEAM1234AB)"Or use environment variables — the recommended approach for CI:
export CSC_LINK=/path/to/certificate.p12
export CSC_KEY_PASSWORD=yourpasswordSet mac.sign.identity: null (or mac.sign: null) to skip signing entirely. Set mac.sign.identity: "-" to use an ad-hoc signature (app will only run on the machine that built it).
:::warning[Ad-hoc signing and Hardened Runtime]
If you disable code signing, you should also disable Hardened Runtime (mac.sign.hardenedRuntime: false), as the combination of no signing and enabled Hardened Runtime may prevent the app from launching.
:::
Hardened Runtime
mac.sign.hardenedRuntime is required for notarization on macOS 10.15+. It restricts what the app can do — you may need entitlements to allow capabilities. The default is true for standard darwin builds and false for Mac App Store builds (mas / mas-dev).
Entitlements
You usually do not need to write an entitlements file at all. The defaults are already minimal and correct for a stock Electron app:
| File being signed | Default entitlements |
|---|---|
| The app bundle | com.apple.security.cs.allow-jit |
| Renderer / GPU helpers | com.apple.security.cs.allow-jit |
| Plugin helper | allow-jit, allow-unsigned-executable-memory, disable-library-validation |
| Frameworks, native modules, unpacked binaries | allow-jit plus osx-sign's Chromium-derived device.* / personal-information.* entitlements (default.darwin.plist) |
Nested binaries do not inherit the app's entitlements. They are handed to @electron/osx-sign, which picks a per-file default modelled on Chromium's own entitlements: the looser exceptions go only to the plugin helper that actually needs them, and every other nested file (frameworks, .node modules, executables in app.asar.unpacked) receives default.darwin.plist — allow-jit plus com.apple.security.device.{audio-input,bluetooth,camera,print,usb} and com.apple.security.personal-information.{location,photos-library}. Outside the plugin helper, none of these defaults grant disable-library-validation or allow-unsigned-executable-memory.
:::warning[Changed in v27]
Earlier versions applied a single entitlements file to the app and every nested binary, granting com.apple.security.cs.allow-unsigned-executable-memory and com.apple.security.cs.disable-library-validation to every process. Modern Electron does not need either in the main process, and both materially weaken the Hardened Runtime, so they are no longer granted by default. If your app depends on them, add them to your own build/entitlements.mac.plist (and build/entitlements.mac.inherit.plist for nested binaries) — see Loading third-party or unsigned binaries below.
:::
Supplying your own entitlements
Create build/entitlements.mac.plist to override the app-level defaults:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
</dict>
</plist>Your file replaces the default rather than extending it, so remember to keep com.apple.security.cs.allow-jit.
A custom build/entitlements.mac.plist applies to the app bundle only — nested binaries keep the per-file defaults described above and do not pick up your custom keys. The inherit plist is for entitlements a nested process needs for itself — for example disable-library-validation on a sidecar executable in app.asar.unpacked that loads third-party libraries of its own, or on a helper process that loads native modules. In that case add build/entitlements.mac.inherit.plist (or set mac.sign.entitlementsInherit). Doing so applies one plist to every nested binary and gives up the per-file defaults, so prefer leaving it absent unless you need it.
Loading third-party or unsigned binaries
com.apple.security.cs.disable-library-validation turns off macOS library validation for the whole process. Grant it only when your app loads a framework, plugin, or native module signed by a different Team ID (or not signed at all) — for example a sidecar binary downloaded at runtime, or one excluded from signing via mac.sign.ignore.
Library validation is enforced by the process that loads the code, based on that process's own entitlements; what the library itself was signed with does not matter. A native module loaded by the main process therefore needs the key in build/entitlements.mac.plist; one loaded by a helper process (a utilityProcess, or a renderer with nodeIntegration) needs it in build/entitlements.mac.inherit.plist as well. The post-sign check below is skipped when the app entitlements grant it, because the main process is the usual loader.
After signing, electron-builder inspects the Mach-O libraries and bundles in app.asar.unpacked and Contents/PlugIns (which electron-builder never re-signs) and warns if any of them carry a foreign or missing signature while the entitlement is absent, so you find out at build time rather than from a launch crash. Executables (such as a bundled ffmpeg) are spawned rather than loaded, so they are not subject to library validation and are not flagged.
Ad-hoc builds (mac.sign.identity: "-") are handled automatically: an ad-hoc signature carries no Team ID, so electron-builder applies a built-in ad-hoc entitlements file that includes disable-library-validation for the app and its nested binaries.
electron-builder auto-detects build/entitlements.mac.plist and build/entitlements.mac.inherit.plist when present. To point at custom paths, set them under mac.sign:
mac:
sign:
entitlements: build/entitlements.mac.plist
entitlementsInherit: build/entitlements.mac.inherit.plistCommon entitlements for Electron apps:
| Entitlement | When Needed |
|---|---|
com.apple.security.cs.allow-jit |
Always — V8 requires JIT (granted by default) |
com.apple.security.cs.allow-unsigned-executable-memory |
Legacy Electron only — not needed by modern V8/Electron and weakens the Hardened Runtime |
com.apple.security.cs.disable-library-validation |
Loading frameworks/native modules signed by another team, or unsigned |
com.apple.security.network.client |
Outgoing network connections (sandboxed apps) |
com.apple.security.network.server |
Listening for connections (sandboxed apps) |
com.apple.security.files.user-selected.read-write |
Open/save panels (sandboxed apps) |
com.apple.security.device.camera |
Camera access |
com.apple.security.device.microphone |
Microphone access |
com.apple.security.app-sandbox |
Required for Mac App Store — see MAS |
:::info[Entitlements and Notarization]
Notarization requires Hardened Runtime + appropriate entitlements. See Notarization for the complete notarization workflow.
:::
Info.plist Customization
Inject arbitrary Info.plist keys using extendInfo:
mac:
extendInfo:
NSMicrophoneUsageDescription: "This app uses the microphone for..."
NSCameraUsageDescription: "This app uses the camera for..."
LSMultipleInstancesProhibited: true
CFBundleURLTypes:
- CFBundleURLSchemes:
- myapp
CFBundleURLName: "com.mycompany.myapp"Helper Bundle IDs
Electron spawns several helper processes, each with its own bundle ID. electron-builder sets these automatically based on your appId, but you can override them:
| Option | Default | Process |
|---|---|---|
helperBundleId |
${appId}.helper |
Generic helper |
helperRendererBundleId |
${appId}.helper.Renderer |
Renderer process |
helperPluginBundleId |
${appId}.helper.Plugin |
Plugin helper |
helperGPUBundleId |
${appId}.helper.GPU |
GPU process |
helperEHBundleId |
${appId}.helper.EH |
Exception handler |
helperNPBundleId |
${appId}.helper.NP |
NP helper |
You only need to override these if you have a specific naming requirement (e.g., for provisioning profiles that enumerate each helper ID explicitly).
Other Common Options
Dark mode: Set darkModeSupport: true if your app supports the system dark mode. This adds the NSRequiresAquaSystemAppearance: false key to Info.plist.
Minimum system version: minimumSystemVersion sets the LSMinimumSystemVersion in Info.plist. Electron itself has a minimum macOS version requirement — don't set this lower than Electron's requirement.
Signing additional binaries: Use mac.sign.binaries to list paths to additional native binaries within your app bundle that need to be signed (e.g., embedded CLIs, helper tools).
mac:
sign:
binaries:
- Contents/MacOS/my-native-helper
- Contents/Frameworks/MyFramework.framework/Versions/A/MyFrameworkProduct / executable name validation: As of v27, electron-builder rejects a productName or executableName that would require filename sanitization (so the generated <Name> Helper.app bundles stay consistent with CFBundleName and Electron's helper discovery). If a build fails with an "is not a valid macOS app bundle name" error, choose a name that needs no sanitization.
Configuration
{!./app-builder-lib.Interface.MacConfiguration.md!}
Mas (`website/docs/mas.md`)
title: "Mac App Store"
{!./partials/_upgrading-from-v26.md!}
The top-level mas key contains a set of options instructing electron-builder on how it should build the MAS (Mac Application Store) target. Inherits all macOS options.
Use the mas-dev target (configured via the top-level masDev key) for local testing of MAS builds with a development provisioning profile.
MAS vs. Direct Distribution
| Aspect | MAS Distribution | Direct Distribution (DMG) |
|---|---|---|
| Certificate | Mac App Distribution | Developer ID Application |
| Notarization | Not required (Apple handles it) | Required (macOS 10.15+) |
| Sandboxing | Mandatory | Optional |
| Update mechanism | Mac App Store | electron-updater |
| Revenue | Apple takes 30% (15% for small dev program) | 100% to you |
| Discovery | App Store search and browsing | Your own marketing |
| Review | Apple review (1-7 days) | Instant |
| macOS version support | As Apple dictates | You control |
Prerequisites
Certificates
You need a Mac App Distribution certificate (for signing the app) and a Mac Installer Distribution certificate (for signing the PKG submitted to App Store Connect). Both are issued from your Apple Developer account.
:::tip
For local testing with mas-dev, you use a Development provisioning profile and a standard developer certificate, not the distribution ones.
:::
Provisioning Profile
MAS apps require a provisioning profile that:
- Lists the specific entitlements your app uses
- Is tied to your App ID and certificate
Create provisioning profiles at developer.apple.com.
mas:
sign:
provisioningProfile: build/MyApp_AppStore.provisionprofile:::note[v27: signing options are under mas.sign]
Like mac, all signing options on mas and masDev moved into a sign object in v27 (sign.provisioningProfile, sign.entitlements, sign.entitlementsInherit, …). electron-builder migrate-schema rewrites the old flat keys. See v27 Breaking Changes → macOS signing.
:::
App Sandbox
All MAS apps must be sandboxed. Add to your entitlements (build/entitlements.mas.plist):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>And build/entitlements.mas.inherit.plist for helper processes:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.inherit</key>
<true/>
</dict>
</plist>Point to these in your configuration:
mas:
sign:
entitlements: build/entitlements.mas.plist
entitlementsInherit: build/entitlements.mas.inherit.plist
provisioningProfile: build/MyApp_AppStore.provisionprofileCommon MAS Entitlements
| Entitlement | Purpose |
|---|---|
com.apple.security.app-sandbox |
Required for MAS |
com.apple.security.network.client |
Outgoing network connections |
com.apple.security.network.server |
Incoming connections |
com.apple.security.files.user-selected.read-write |
Open/Save panels |
com.apple.security.files.downloads.read-write |
Downloads folder access |
com.apple.security.device.camera |
Camera |
com.apple.security.device.microphone |
Microphone |
com.apple.security.personal-information.contacts |
Contacts |
com.apple.security.personal-information.calendars |
Calendar |
com.apple.security.print |
Printing |
com.apple.security.automation.apple-events |
AppleScript/Apple Events |
Testing with mas-dev
The mas-dev target produces a build signed with a development certificate and development provisioning profile — suitable for testing sandbox behavior on your machine without going through App Store review.
masDev:
sign:
provisioningProfile: build/MyApp_Dev.provisionprofile
entitlements: build/entitlements.mas.plist
entitlementsInherit: build/entitlements.mas.inherit.plistBuild the dev target:
electron-builder --mac mas-devBuilding for the App Store
electron-builder --mac masThis produces:
MyApp-<version>.pkg— the package to upload to App Store Connect
Submitting to App Store Connect
- Open App Store Connect
- Create your app listing (if new) under My Apps → +
- Use Transporter (free, from the Mac App Store) or
xcrun altool/xcrun notarytoolto upload the.pkg - The uploaded build appears in App Store Connect after processing (usually a few minutes)
- Select the build for your release and submit for review
Common Review Rejection Reasons
- Sandbox violations — the app attempts to access files or resources not permitted by entitlements
- Deprecated APIs — using APIs Apple has removed or flagged (check release notes)
- Missing privacy strings — if you access camera/mic/location,
NSCameraUsageDescriptionetc. must be inInfo.plist - UI guidelines violations — buttons, windows, or flows that don't match Human Interface Guidelines
- Crash on launch — always test the MAS build on a clean machine before submitting
Configuration
{!./app-builder-lib.Interface.MasConfiguration.md!}
Msi Wrapped (`website/docs/msi-wrapped.md`)
title: "MSI Wrapped"
The top-level msiWrapped key contains a set of options instructing electron-builder on how it should build Windows MSI-Wrapped packages.
What is MSI-Wrapped?
MSI-Wrapped is a hybrid approach that combines the customizability of NSIS with the enterprise deployment compatibility of MSI. It works by:
- Building your normal NSIS installer
- Wrapping the NSIS
.exeinside a minimal MSI package - The MSI acts as a thin shell that executes the NSIS installer when deployed
This allows enterprise IT teams to deploy the package through MSI-compatible tools (Group Policy, SCCM, Intune) while you retain all of NSIS's powerful customization capabilities for the actual installation experience.
MSI-Wrapped vs. Native MSI
| Aspect | MSI-Wrapped | Native MSI |
|---|---|---|
| Underlying installer | NSIS | WiX-generated MSI |
| NSIS customization | Full | None |
| Enterprise MSI deployment | Yes | Yes |
| Silent install | Via NSIS flags | Via MSI /quiet |
| Windows Store (MSIX) | Limited | Better |
| Complexity | Lower | Higher |
When to Use MSI-Wrapped
Choose MSI-Wrapped when:
- You already have a well-configured NSIS installer and need to deploy it via Group Policy or MDM
- Your enterprise customers require MSI format but you don't want to maintain a separate MSI configuration
- You need NSIS-level UI customization combined with MSI deployment
Choose native MSI when:
- You need full MSI capabilities (custom actions, rollback, MSI database manipulation)
- You're targeting Windows Store / MSIX conversion
- You need clean per-machine/per-user support without NSIS
Configuration
Wrapped Installer Arguments
Pass additional command-line arguments to the wrapped NSIS installer when it is executed:
msiWrapped:
wrappedInstallerArgs: "/S /D=C:\\MyApp"Impersonation
msiWrapped:
impersonate: false # Default: falseWhen impersonate: true, the MSI wrapper executes the NSIS installer under the user's identity rather than the system/SYSTEM account. This is useful for per-user installations that need to access user-specific resources.
Upgrade Code
msiWrapped:
upgradeCode: "{A1234567-BCDE-F012-3456-789ABCDEF012}":::warning[Critical: Do Not Change After First Release]
See the MSI upgrade code warning — the same applies here.
:::
Complete Example
win:
target:
- nsis
- msi-wrapped
nsis:
oneClick: false
perMachine: true
allowToChangeInstallationDirectory: true
msiWrapped:
upgradeCode: "{A1234567-BCDE-F012-3456-789ABCDEF012}"
impersonate: false
wrappedInstallerArgs: "/SILENT"Configuration
{!./app-builder-lib.Interface.MsiWrappedOptions.md!}
Msi (`website/docs/msi.md`)
The top-level msi key contains a set of options instructing electron-builder on how it should build Windows MSI (Microsoft Installer) packages.
When to Use MSI
MSI packages use Windows Installer technology and are the standard format for enterprise software deployment:
- Group Policy deployment — IT administrators can silently push MSI packages to managed machines via Active Directory
- SCCM / Intune / MDM — Microsoft's enterprise management tools work natively with MSI
- Silent installation — standard
/quietand/passiveflags work without custom configuration - Windows Store compatibility — MSI can be used as a basis for MSIX packaging (see MSI-Wrapped)
- Upgrade/downgrade control — Windows Installer handles version management via upgrade codes
MSI vs. NSIS vs. Portable
| Aspect | MSI | NSIS | Portable |
|---|---|---|---|
| Enterprise deployment | Excellent | Limited | None |
| Silent install | /quiet flag |
Custom scripting | N/A |
| Group Policy | Yes | No | No |
| Customizability | Moderate (WiX) | Excellent | N/A |
| UI flexibility | Limited | Highly customizable | N/A |
| File size | Typically larger | Compressed | No overhead |
| Auto-update | Not supported via electron-updater | electron-updater | Manual |
| Recommended for | Enterprise | Consumer | USB/no-install |
Upgrade Code
The upgradeCode is a GUID that uniquely identifies your product across all versions. It persists through upgrades and is used by Windows Installer to find and upgrade existing installations.
:::warning[Critical: Do Not Change After First Release]
If you change the upgradeCode after your first release, Windows Installer will treat the new version as a completely different product — existing installations will NOT be upgraded, and users will end up with both versions installed. Set this once and never change it.
:::
msi:
upgradeCode: "{A1234567-BCDE-F012-3456-789ABCDEF012}"If not specified, a deterministic GUID is generated from the app ID (appId). Explicitly setting it is strongly recommended for production.
One-Click Installation
msi:
oneClick: true # Default: trueWhen oneClick is true, the installer runs without showing UI (silent install). Set to false for a traditional wizard-style installer.
Per-Machine vs. Per-User Installation
msi:
perMachine: false # Default: false (per-user install)perMachine: false— installs for the current user only, no admin requiredperMachine: true— installs for all users, requires administrator privileges
For enterprise deployment via Group Policy, perMachine: true is typically required.
Desktop and Start Menu Shortcuts
Shortcut creation is controlled via the inherited CommonWindowsInstallerConfiguration options:
msi:
createDesktopShortcut: true # Default: true
createStartMenuShortcut: true # Default: true
menuCategory: "My Company" # Creates a subfolder in Start Menu
shortcutName: "My App" # Overrides default shortcut name
runAfterFinish: true # Launch app after installation completesWiX Customization
The MSI target uses WiX Toolset internally. You can pass additional arguments to the WiX compiler (candle.exe) and linker (light.exe):
msi:
additionalWixArgs:
- "-dMyVariable=MyValue"
- "-ext WixUIExtension"
additionalLightArgs:
- "-sice:ICE80"Use msiProjectCreated hook to modify the generated WiX project files before they are compiled:
msiProjectCreated: ./msi-hook.jsWarnings as Errors
msi:
warningsAsErrors: true # Default: trueWiX compiler warnings are treated as errors by default. Set to false if you're using advanced WiX features that trigger known benign warnings.
Silent Installation
MSI packages support standard Windows Installer command-line flags for silent deployment:
# Silent install (no UI)
MyApp-Setup.msi /quiet
# Passive install (progress bar only)
MyApp-Setup.msi /passive
# Silent uninstall
msiexec /x {PRODUCT-CODE} /quiet
# Install per-machine
MyApp-Setup.msi /quiet ALLUSERS=1
# Install to custom path
MyApp-Setup.msi /quiet INSTALLDIR="C:\MyCustomPath\MyApp"Complete Example
win:
target:
- msi
msi:
oneClick: true
perMachine: true
upgradeCode: "{A1234567-BCDE-F012-3456-789ABCDEF012}"
createDesktopShortcut: true
createStartMenuShortcut: true
menuCategory: "My Company"
runAfterFinish: false # Don't auto-launch after silent enterprise install
warningsAsErrors: trueTroubleshooting
Build fails with WiX error: Enable verbose output with DEBUG=electron-builder and check the WiX error codes. Common issues include missing files or invalid GUIDs.
Upgrade installs alongside old version: Verify the upgradeCode is set and has not changed between versions. Also ensure ProductCode (auto-generated per version) is different.
Silent install shows UI: Confirm oneClick: true is set and the /quiet flag is being used. Some antivirus software intercepts installer launches.
Per-machine install fails without admin: Per-machine installation requires elevation. Ensure the deployment mechanism (SCCM, Intune, GPO) runs the installer in a privileged context.
Configuration
{!./app-builder-lib.Interface.MsiOptions.md!}
Msix (`website/docs/msix.md`)
The top-level msix key contains a set of options instructing electron-builder on how it should build MSIX packages for the Microsoft Store, enterprise sideloading, and modern Windows deployment.
:::info[Beta]
The MSIX target is beta. The configuration surface and generated manifest are still evolving. Pin your electron-builder version and re-test packaging after upgrades.
:::
All options are optional. All required MSIX configuration is inferred and computed automatically from your application metadata.
MSIX vs. AppX
MSIX is the modern successor to AppX. They share the same underlying manifest format (AppxManifest.xml), the same packaging tool (makeappx.exe), and the same signing requirements, but MSIX exposes newer manifest schemas and deployment features that AppX does not.
| Capability | AppX (appx) | MSIX (this target) |
|---|---|---|
| Artifact | .appx |
.msix, .msixbundle, .msixupload |
| Multi-arch bundle | No | Yes (.msixbundle) |
| Store upload archive | Manual | Yes (.msixupload) |
Package Integrity (uap10) |
No | Yes (enforcePackageIntegrity) |
Windows Services (desktop6) |
No | Yes (windowsServices) |
| Required toolset | any | winCodeSign ≥ 1.0.0 |
If you are starting fresh, prefer MSIX. AppX remains for backward compatibility.
When to Use MSIX
Choose MSIX when:
- Windows Store distribution — your app will be listed in the Microsoft Store.
- Enterprise sideloading — IT manages app distribution via MDM/Intune without the Store.
- Windows 10S / Windows SE — these locked-down editions only run Store-packaged apps.
- You need modern packaging features — package integrity, bundled architectures, or registered Windows services.
For standard consumer distribution with self-managed auto-updates, NSIS is simpler. For enterprise deployment without the Store, MSI is often preferred.
Requirements
MSIX packaging requires a modern Windows Kits toolset that bundles makeappx.exe, makepri.exe, and the MSIX-capable Windows SDK. Set toolsets.winCodeSign to any modern bundle >= 1.0.0 (e.g. 1.0.0, 1.1.0, or 1.3.0):
toolsets:
winCodeSign: "1.1.0"
win:
target: msix:::warning
The legacy winCodeSign-2.6.0 bundle (toolset 0.0.0) does not include the SDK version required for MSIX. Building MSIX without a modern toolset throws an InvalidConfigurationError.
:::
MSIX can be built on:
- Windows 10 / Windows Server 2012 R2 (version 6.3+) or later — native.
- macOS via Parallels Desktop — electron-builder drives a Windows VM through Parallels to run the Windows-only packaging tools. See Building on macOS below.
Linux is not supported for MSIX.
Quick Start
win:
target: msix
toolsets:
winCodeSign: "1.1.0"
msix:
identityName: "MyCompany.MyApp"
publisher: "CN=My Company, O=My Company, C=US"
publisherDisplayName: "My Company"
displayName: "My Application"electron-builder --win msixMSIX Assets
MSIX uses the same logo/icon assets as AppX. Place them in the appx folder inside your build resources directory (default: build/appx/):
| Asset | File Name | Required | Size |
|---|---|---|---|
| Store Logo | StoreLogo.png |
Yes | 50×50 |
| Square 150×150 | Square150x150Logo.png |
Yes | 150×150 |
| Square 44×44 | Square44x44Logo.png |
Yes | 44×44 |
| Wide 310×150 | Wide310x150Logo.png |
Yes | 310×150 |
| Badge Logo | BadgeLogo.png |
Optional | 24×24 |
| Large Tile | LargeTile.png |
Optional | 310×310 |
| Small Tile | SmallTile.png |
Optional | 71×71 |
| Splash Screen | SplashScreen.png |
Optional | 620×300 |
Default assets are substituted for the required logos if you don't provide your own. Scale variants (e.g., Square44x44Logo.scale-200.png, BadgeLogo.targetsize-48.png) are supported — when any scaled asset is present, electron-builder runs makepri.exe to generate resources.pri automatically. See Microsoft's tile asset guidelines.
Identity and Publisher
msix:
applicationId: "MyApp" # alpha-numeric segments separated by periods, each segment starting with a letter
identityName: "MyCompany.MyApp" # unique within the Store; 3–50 chars, [a-zA-Z0-9.-]
publisher: "CN=My Company, O=My Company, C=US" # must match certificate Subject exactly
publisherDisplayName: "My Company" # friendly publisher name shown to users
displayName: "My Application" # shown in Start menu / StoreDefaults when omitted:
applicationId→ derived fromidentityName(a leading numeric prefix such as01234is stripped automatically, with a warning), falling back to the application name. Validated to 1–64 characters.identityName→ the applicationname. Validated to 3–50 characters of[a-zA-Z0-9.-].publisherDisplayName→ company name from your application metadata (authorinpackage.json). Building fails if neither is set.
:::warning[Publisher Must Match Certificate]
The publisher value must exactly match the Subject field of your code signing certificate. Mismatches cause installation failures. When building for the Store (no certificate configured), electron-builder uses CN=ms as a placeholder — the Store re-signs on submission.
:::
Capabilities
MSIX apps declare the system capabilities they use:
msix:
capabilities:
- runFullTrust # required for Electron apps — always included
- internetClient # outbound internet access
- privateNetworkClientServer # local network
- webcam # camera access
- microphone # microphone accessrunFullTrust is required for Electron apps and is always added, even if you specify a different set — you cannot remove it. Invalid capability names cause the build to fail with invalid windows capabilities. The full list is in Microsoft's capability documentation.
Tile Customization
msix:
backgroundColor: "#2c2c2c" # tile background color (CSS color). Default: #464646
showNameOnTiles: false # overlay app name on the wide & square tiles. Default: falseOptional tile assets enable extra visual elements automatically when present in build/appx/: BadgeLogo.png (lock-screen badge), LargeTile.png (310×310), SmallTile.png (71×71), and SplashScreen.png.
Language Support
msix:
languages:
- en-US # first entry is the default language
- de-DE
- fr-FR
- ja-JPDeclares which languages your app supports for Store listings and localized resources. Defaults to ["en-US"]. Underscores are normalized to hyphens (en_US → en-US).
Version Targeting
msix:
minVersion: "10.0.17763.0" # Windows 10 1809 minimum (Store minimum). This is the default.
maxVersionTested: "10.0.22621.0" # defaults to minVersion when omittedThese map to MinVersion / MaxVersionTested on <TargetDeviceFamily Name="Windows.Desktop" />.
msix:
setBuildNumber: false # include the build number in the package version. Default: falsePackage Integrity
msix:
enforcePackageIntegrity: true # Default: falseWhen enabled, emits inside. The child `` element is what actually turns on runtime integrity checks — a bare/childless element is a no-op. Windows then verifies that no package files have been tampered with after signing. Requires Windows 10 version 2004 (build 19041) or later.
Windows Services
Register Windows Services that ship inside the package (desktop6 schema). Requires Windows 10 version 1903 (build 18362) or later, and the packagedServices (or localSystemServices) restricted capability declared via capabilities — services will not register without it.
msix:
capabilities:
- packagedServices # restricted capability — required for windows.service
windowsServices:
- name: "MyBackgroundService" # required — Service Control Manager name
executable: "app/service.exe" # optional — defaults to the main app executable
startupType: "auto" # auto | manual | disabled. Default: manual
startAccount: "localSystem" # localSystem | localService | networkService. Default: localSystem
arguments: "--mode service" # optional — command-line arguments| Field | Required | Default | Description |
|---|---|---|---|
name |
Yes | — | Service name used in the Windows Service Control Manager. |
executable |
No | main app executable | Relative path to the service executable within the package. |
startupType |
No | manual |
How Windows starts the service: auto, manual, or disabled. |
startAccount |
No | localSystem |
The account the service runs under: localSystem, localService, or networkService. Required by the desktop6:Service schema and always emitted. |
arguments |
No | — | Command-line arguments passed to the service executable. |
Each entry produces a <desktop6:Extension Category="windows.service" Executable="..." EntryPoint="Windows.FullTrustApplication"><desktop6:Service Name="..." StartupType="..." StartAccount="..." Arguments="..." /></desktop6:Extension> declaration.
Auto-Launch Extension
msix:
addAutoLaunchExtension: trueWhen true, adds a desktop:Extension Category="windows.startupTask" so the app runs on user login. When unset, it defaults to true only if electron-winstore-auto-launch is present in your dependencies, otherwise false. Requires the runFullTrust capability (always present).
Protocols and File Associations
MSIX picks up the top-level protocols and fileAssociations configuration and emits the corresponding uap:Extension declarations — you do not configure these under msix:
protocols:
name: "My App Protocol"
schemes: ["myapp"]
fileAssociations:
- ext: "myext"
name: "My Custom File"Custom Manifest and Extensions
For advanced scenarios, supply your own manifest template or inject extra XML:
msix:
customManifestPath: "msix/AppxManifest.xml" # relative to build resources
customExtensionsPath: "msix/extensions.xml" # injected into <Extensions>The custom manifest supports the same ${} macros as the default template, including the MSIX-specific macro ${packageIntegrity}. Windows services are injected via the ${extensions} macro alongside protocol/file-association extensions.
Note the two paths resolve against different base directories: customManifestPath is relative to the build resources directory, whereas customExtensionsPath is resolved relative to the app source directory (the packaged app dir), not the build resources directory.
:::warning[Namespaces in a custom template]
If you supply your own template, it must declare the namespaces used by the features you enable — desktop (auto-launch startup task), uap (protocols / file associations), uap10 (package integrity), and desktop6 (services) — and list the appropriate prefixes in IgnorableNamespaces. Omitting them produces an invalid manifest. Start from the default template above.
:::
Bundles and Store Uploads
When you build more than one architecture, electron-builder can combine the per-architecture .msix packages into a single .msixbundle, and optionally wrap it into a .msixupload archive for the Microsoft Store Partner Center:
win:
target:
- target: msix
arch:
- x64
- arm64
msix:
createMsixbundle: true # combine architectures into one .msixbundle. Default: true (when >1 arch)
createMsixupload: true # produce a .msixupload for Partner Center submission. Default: false- A
.msixbundleis produced only when more than one architecture is built andcreateMsixbundleis notfalse. - A
.msixuploadis a plain ZIP of the bundle (or, if no bundle was produced, the single package). It is not code-signed — the Store re-signs on submission.
:::note
If you set createMsixbundle: false while building multiple architectures and request createMsixupload: true, the upload will contain only the first architecture's package. Keep createMsixbundle enabled for multi-architecture Store submissions.
:::
Code Signing {#msix-package-code-signing}
MSIX artifacts (.msix, .msixbundle) are signed with SHA-256 (single signature; dual SHA-1/SHA-256 signing does not apply to MSIX).
- Store distribution — no manual signing needed. The Store signs the package with a Microsoft certificate during submission; the
publisheris replaced with a Store-managed identity. - Sideloading / enterprise — the package must be signed with a trusted certificate whose Subject matches
publisher. See Windows Code Signing.
For self-signed certificates during development, see Microsoft's certificate guide.
Building on macOS
MSIX packaging tools are Windows-only. On macOS, electron-builder runs them inside a Windows VM via Parallels Desktop. Temp files are routed through your home directory so the VM can access them over the always-available \\Mac\Home share. Ensure a Parallels Windows VM is configured and the modern winCodeSign toolset is available inside it.
Testing Locally
# Install locally (Developer Mode must be enabled for unsigned packages, or import the signing cert first)
Add-AppxPackage -Path ".\MyApp-1.0.0.msix"
# Install a bundle
Add-AppxPackage -Path ".\MyApp-1.0.0.msixbundle"
# Verify it is registered
Get-AppxPackage | Where-Object { $_.Name -like "*MyApp*" }
# Inspect the installed manifest
$p = Get-AppxPackage | Where-Object { $_.Name -like "*MyApp*" } | Select-Object -First 1
(Get-AppxPackageManifest -Package $p.PackageFullName).OuterXml
# Uninstall
Get-AppxPackage | Where-Object { $_.Name -like "*MyApp*" } | Remove-AppxPackageTo install a package signed with a self-signed certificate, first import that certificate into Trusted People on the target machine — see Microsoft's signing guide.
Publishing to the Microsoft Store
- Create a Microsoft Developer account and reserve your app name in Partner Center.
- Build with
createMsixupload: trueto produce a.msixupload. - Upload the
.msixuploadvia the Partner Center submission flow. - Complete the submission (age rating, pricing, screenshots) and wait for certification.
Auto-Updates
MSIX auto-updates are handled by the Microsoft Store (or your App Installer .appinstaller flow), not by electron-updater. For self-managed auto-updates outside the Store, use NSIS instead.
Common Questions
Which toolset version do I need? toolsets.winCodeSign can be any modern bundle >= 1.0.0 (e.g. 1.0.0, 1.1.0, or 1.3.0). Only an unset toolset or the legacy 0.0.0 bundle (winCodeSign-2.6.0, which lacks the required SDK) is rejected.
Can I build MSIX on Linux? No. Use Windows, or macOS with Parallels Desktop.
Why is my multi-arch .msixbundle named with one architecture? Bundle and upload artifacts span all built architectures; a custom artifactName containing ${arch} will still render a single architecture token. Prefer an artifactName without ${arch} for bundles.
Does MSIX support auto-update via electron-updater? No — see Auto-Updates above.
Configuration
{!./app-builder-lib.Interface.MsixOptions.md!}
Notarization (`website/docs/notarization.md`)
macOS Notarization
Notarization is Apple's security service that scans your app for malicious software before distribution. Starting with macOS 10.15 (Catalina), apps distributed outside the Mac App Store must be notarized or Gatekeeper will block them.
Notarization vs. Code Signing
These are two separate steps, both required:
| Step | What it does | Required since |
|---|---|---|
| Code signing | Proves the app came from you and wasn't modified | macOS 10.8 (Mountain Lion) |
| Notarization | Apple scans your app for malware; issues a ticket | macOS 10.15 (Catalina) |
| Stapling | Attaches the notarization ticket to the app bundle | Recommended (enables offline verification) |
electron-builder handles all three steps automatically when configured.
Prerequisites
Before notarizing, you need:
- An active Apple Developer Program membership ($99/year)
- A Developer ID Application certificate (not Mac App Store)
- Hardened Runtime enabled in your build configuration
- Required entitlements set (see below)
- Authentication credentials (Apple ID + app-specific password, or API key)
Enabling Notarization
mac:
notarize: true
sign:
hardenedRuntime: true
entitlements: build/entitlements.mac.plist
entitlementsInherit: build/entitlements.mac.inherit.plist:::note[v27]notarize stays on mac, but the signing options (hardenedRuntime, entitlements, entitlementsInherit) moved under mac.sign in v27. See macOS signing.
:::
Authentication — Option A: Apple ID
Create an app-specific password at appleid.apple.com (do not use your Apple ID password directly).
Set these environment variables:
[email protected]
APPLE_APP_SPECIFIC_PASSWORD=xxxx-xxxx-xxxx-xxxx
APPLE_TEAM_ID=ABCDE12345Authentication — Option B: App Store Connect API Key (Recommended for CI)
API keys don't expire and don't require two-factor authentication, making them ideal for CI.
- Go to App Store Connect → Users and Access → Keys
- Create a new key with "Developer" role
- Download the
.p8file (only downloadable once) - Note the Key ID and Issuer ID
Then set the following environment variables:
# base64-encode the .p8 file for use as a CI secret
APPLE_API_KEY=<base64-encoded-p8-content>
APPLE_API_KEY_ID=KEYID
APPLE_API_ISSUER=your-issuer-id
APPLE_TEAM_ID=ABCDE12345Authentication — Option C: Keychain Profile (stored credentials)
If you have previously stored notarization credentials via xcrun notarytool store-credentials, reference them by profile name:
APPLE_KEYCHAIN_PROFILE=my-notarization-profile
APPLE_KEYCHAIN=/path/to/keychain.keychain-db # optional; defaults to system keychainThis approach requires credentials to be pre-stored on the build machine and is not suitable for ephemeral CI environments.
Hardened Runtime Requirements
Notarization requires Hardened Runtime (mac.sign.hardenedRuntime: true). This restricts what your app process can do unless you explicitly declare entitlements.
Modern Electron needs exactly one exception, and electron-builder grants it by default — no entitlements file is required to notarize a stock Electron app:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
</dict>
</plist>Write that to build/entitlements.mac.plist only if you need to add capabilities of your own; your file replaces the default rather than extending it, so keep com.apple.security.cs.allow-jit in it.
Do not add com.apple.security.cs.allow-unsigned-executable-memory out of habit. It was needed by Electron versions from the 2019 era, is not needed by modern V8/Electron, and weakens the Hardened Runtime for no benefit.
Leave build/entitlements.mac.inherit.plist absent unless you truly need it. Without it, each nested binary is signed with @electron/osx-sign's per-file defaults, which mirror Chromium's own entitlements — renderer and GPU helpers get only allow-jit, and the looser exceptions go to the plugin helper alone. Supplying the file applies one plist to every nested binary instead.
Common Entitlements
| Entitlement | When Needed |
|---|---|
com.apple.security.cs.allow-jit |
Required by Electron (V8 JIT) — granted by default |
com.apple.security.cs.allow-unsigned-executable-memory |
Legacy Electron only — not needed by modern V8/Electron and weakens the Hardened Runtime |
com.apple.security.cs.disable-library-validation |
Loading frameworks/native modules signed by another team, or unsigned |
com.apple.security.network.client |
Outbound network access |
com.apple.security.network.server |
Incoming connections |
com.apple.security.files.user-selected.read-write |
Read/write files the user selects |
com.apple.security.files.downloads.read-write |
Read/write the Downloads folder |
com.apple.security.device.camera |
Camera access |
com.apple.security.device.microphone |
Microphone access |
com.apple.security.personal-information.location |
Location services |
Notarization in GitHub Actions
jobs:
build-mac:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Build, sign, and notarize
run: npx electron-builder --mac
env:
CSC_LINK: ${{ secrets.MAC_CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.MAC_CSC_KEY_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}Required GitHub Secrets
| Secret | Value |
|---|---|
MAC_CSC_LINK |
Base64-encoded .p12 certificate |
MAC_CSC_KEY_PASSWORD |
Certificate password |
APPLE_ID |
Your Apple ID email |
APPLE_APP_SPECIFIC_PASSWORD |
App-specific password from appleid.apple.com |
APPLE_TEAM_ID |
Your 10-character team ID (e.g., ABCDE12345) |
How Stapling Works
After notarization, Apple returns a "ticket" that confirms your app was approved. electron-builder automatically staples this ticket to the app bundle — meaning the notarization check can succeed even without an internet connection.
Stapling is performed automatically when notarize: true. You don't need to configure anything extra.
Testing Notarization
After building, verify the notarization status:
# Check Gatekeeper assessment (simulates what macOS does on first launch)
spctl --assess --verbose --type exec dist/mac/MyApp.app
# Should output:
# dist/mac/MyApp.app: accepted
# source=Notarized Developer ID
# Check that the stapling ticket is attached
xcrun stapler validate dist/mac/MyApp.app
# Should output: The validate action worked!
# Check code signature
codesign --verify --deep --strict --verbose=2 dist/mac/MyApp.appTroubleshooting
"The software certificate used to sign this application is not trusted on this machine"
: Your Developer ID certificate is not in the system keychain or hasn't been trusted. Verify CSC_LINK points to a valid Developer ID certificate — electron-builder imports and configures the certificate automatically.
Notarization hangs indefinitely
: Apple's notarization service may be slow. electron-builder polls for completion. If it consistently times out, check your network connection and Apple's developer system status.
"Package Invalid: the binary is not signed with a valid Developer ID certificate"
: You used a Mac App Store certificate for direct distribution, or vice versa. Direct distribution requires a "Developer ID Application" certificate, not "Apple Distribution."
"App can't be opened because Apple cannot check it for malicious software"
: Notarization succeeded but the app wasn't stapled, or the user opened the app before stapling completed. Run xcrun stapler validate to check. Rebuild with electron-builder to get a properly stapled app.
"com.apple.security.cs.allow-jit entitlement required"
: Add the JIT entitlement to your entitlements.mac.plist. See the entitlements section above.
Notarization rejected: "The executable does not have the Hardened Runtime enabled"
: Set mac.sign.hardenedRuntime: true in your mac configuration.
"Invalid Info.plist (plist or signature have wrong format)"
: All binaries inside the app bundle must be signed. This often happens with third-party native modules or bundled binaries. Ensure asar.unpack is set for any .node files and that all binaries are signed before creating the final distributable. Use a custom afterSign hook if you need to sign additional binaries.
Related Pages
- macOS Code Signing — exporting certificates, keychain management, disabling signing
- Code Signing Overview — environment variables, CI setup, certificate types
- macOS Configuration — entitlements, Hardened Runtime, bundle ID setup
- GitHub Actions — full workflow examples
Nsis (`website/docs/nsis.md`)
title: "NSIS"
The top-level nsis key contains a set of options instructing electron-builder on how it should build NSIS target (default target for Windows).
These options are also applicable for the Web installer, use top-level nsisWeb key.
Unicode enabled by default. Large strings are supported (maximum string length of 8192 bytes instead of the default of 1024 bytes).
32 bit + 64 bit
If you build both ia32 and x64 arch (--x64 --ia32), you will always get one installer. The appropriate arch will be installed automatically.
The same applied to web installer (nsis-web target). The same dual-arch installer mechanism also applies to --x64 --arm64.
:::note[ia32 requires Electron <= 43]
Electron 44 removed Windows ia32 builds — building ia32 requires electronVersion <= 43.x (supported until the v43 series reaches end-of-life in January 2027).
:::
Web Installer
To build web installer, set target to nsis-web. Web Installer automatically detects OS architecture and downloads corresponding package file. So, the user doesn't need to guess what installer to download and at the same time you don't bundle package files for all architectures in one installer (as in case of default nsis target). It doesn't matter for common Electron application (due to superb LZMA compression, size difference is acceptable), but if your application is huge, Web Installer is a solution.
To customize web installer, use the top-level nsisWeb key (not nsis).
If for some reasons web installer cannot download (antivirus, offline):
- Download package file into the same directory where installer located. It will be detected automatically and used instead of downloading from the Internet. Please note — only original package file is allowed (checksum is checked).
- Specify a local package file using
--package-file=path_to_file. It must be one of the packages built with the installer (any arch; checksum is checked).
The web installer verifies the package it installs against the SHA-512 of the packages built with it:
- A package passed via
--package-file(electron-updater passes the package it verified against the update manifest) must match one of them, for any arch. - A package downloaded from the default, publish-derived (versioned) URL must match the package for the detected arch.
- A package downloaded from an explicit
appPackageUrl(e.g. a version-independentlatestURL) is not verified, since that URL can serve packages of other builds.
A local package (passed via --package-file, or found next to the installer) is first copied into the installer's own temporary directory. The installer verifies and installs that copy. The local file itself is left in place; for an update, electron-updater removes the package it passed once the updated app starts. With allowUnverifiedAppPackage a package passed via --package-file is still copied, but not verified.
A mismatch, a --package-file package that cannot be copied, or a package that cannot be downloaded (also a download cancelled in the progress window) aborts the installation (exit code 2); a package next to the installer that doesn't match or cannot be copied is ignored and the package is downloaded instead. When a download fails, an interactive run first offers to retry, and a silent run (/S) aborts right away. The installer copies, verifies and, if needed, downloads the package before it removes an installed version, so an installation aborted for one of these reasons leaves that version in place. Because the download can take a while, a run that isn't an update (--updated) then checks again whether the app is running, with the same prompt as at the start (Cancel aborts with exit code 2), before the installed version is removed; a custom customCheckAppRunning macro is not run a second time. Set nsisWeb.allowUnverifiedAppPackage: true (default false) to skip the --package-file and versioned-download checksum checks (a package next to the installer is still checked) only if you intentionally use one web installer with packages of other builds (e.g. a version-independent installer with --package-file).
For auto-update, electron-updater installs web-installer updates only when disableWebInstaller is false; installs made by an nsis-web installer built with electron-builder v27+ default to false automatically — see Auto Update. Don't set updateManifest: false for nsis-web apps: signed update manifests cover the web package's path, SHA-512 and size.
Custom NSIS script
Two options are available — include and script. script allows you to provide completely different NSIS script. For most cases it is not required as you need only to customise some aspects, but still use well-tested and maintained default NSIS script. So, include is recommended.
:::warning[Custom script disables built-in safeguards]
When you provide a custom script, electron-builder no longer generates (and signs) the uninstaller for you and skips installer size verification. Prefer include unless you really need to replace the whole script.
:::
For nsis-web, a custom script that copies installSection.nsh should insert !insertmacro prepareWebPackage before uninstallOldVersion; otherwise the package is prepared in installApplicationFiles, after the installed version is removed.
Keep in mind — if you customize the NSIS script, you should always mention it in issue reports. And don't expect that your issue will be resolved.
:::warning[v27: file-association ProgID format changed]
NSIS installers now register each fileAssociations entry under a unique generated ProgID (.<component>, derived from productName + the app GUID) instead of using the association name/extension verbatim, which could collide with unrelated apps. fileAssociations and its name/ext/description fields are unchanged and nothing needs migrating — but if your custom include/script (or external tooling) hard-codes the old ProgID (the association name or extension) to add shell verbs or registry keys, update it to the new generated value. See v27 Breaking Changes → NSIS file-association ProgID.
:::
- Add file
build/installer.nsh(or set include explicitly — a single path, or an array of paths that are all included in order, e.g."include": ["build/installer.nsh", "build/signing.nsh"]; each path is resolved relative to the build resources directory first, then relative to the project directory). - Define wanted macro to customise:
customHeader,preInit,customInit,customUnInit,customInstall,customUnInstall,customRemoveFiles,customInstallMode,customWelcomePage,customUnWelcomePage,customUnInstallSection.
:::note[Example]
!macro customHeader
!system "echo '' > ${BUILD_RESOURCES_DIR}/customHeader"
!macroend
!macro preInit
; This macro is inserted at the beginning of the NSIS .OnInit callback
!system "echo '' > ${BUILD_RESOURCES_DIR}/preInit"
!macroend
!macro customInit
!system "echo '' > ${BUILD_RESOURCES_DIR}/customInit"
!macroend
!macro customInstall
!system "echo '' > ${BUILD_RESOURCES_DIR}/customInstall"
!macroend
!macro customInstallMode
# set $isForceMachineInstall or $isForceCurrentInstall
# to enforce one or the other modes.
!macroend
!macro customWelcomePage
# Welcome Page is not added by default for installer.
!insertMacro MUI_PAGE_WELCOME
!macroend
!macro customUnWelcomePage
!define MUI_WELCOMEPAGE_TITLE "custom title for uninstaller welcome page"
!define MUI_WELCOMEPAGE_TEXT "custom text for uninstaller welcome page $\r$\n more"
!insertmacro MUI_UNPAGE_WELCOME
!macroend
!macro customUnInstallSection
Section /o "un.Some cool checkbox"
; You can add some uninstall section as component page
; If defined, then always run after `customUnInstall`
SectionEnd
!macroend:::
BUILD_RESOURCES_DIRandPROJECT_DIRare defined.buildis added asaddincludedir(i.e. you don't need to useBUILD_RESOURCES_DIRto!includesibling files from the build resources directory).build/x86-unicodeandbuild/x86-ansiare added asaddplugindir(each one only when the directory exists, regardless of theunicodeoption).- File associations macro
registerFileAssociationsandunregisterFileAssociationsare still defined. - All other electron-builder specific flags (e.g.
ONE_CLICK) are still defined.
:::note[Uninstaller lifecycle — customUnInstall changes take effect one version later]
The uninstaller that runs during an uninstall or during an update is the Uninstall <app>.exe that was written to disk by the previously installed version — not the one embedded in the installer that is currently running. So when you add or change customUnInstall (or anything else affecting the uninstaller), the change only becomes active after the next install: version N ships the new uninstaller, and it is first executed when version N is uninstalled or updated to N+1. If your customUnInstall "does not fire", it is almost always because the machine still runs the old uninstaller from the previous version.
:::
:::warning[Uninstall registry key — do not hard-code ...\Uninstall\<appId>]
electron-builder registers the uninstall entry under Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_APP_KEY}, where UNINSTALL_APP_KEY is the application GUID (with each \ replaced by " - ", i.e. space, hyphen, space) — _not_ the appId. In a custom script use the provided defines instead of building the path yourself: ${UNINSTALL_REGISTRY_KEY} (the full key, see multiUser.nsh) or ${UNINSTALL_APP_KEY}. A hard-coded Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_ID} points at a key electron-builder never writes.
:::
If you want to include additional resources for use during installation, such as scripts or additional installers, you can place them in the build directory and include them with File. For example, to include and run extramsi.msi during installation, place it in the build directory and use the following:
!macro customInstall
File /oname=$PLUGINSDIR\extramsi.msi "${BUILD_RESOURCES_DIR}\extramsi.msi"
ExecWait '"msiexec" /i "$PLUGINSDIR\extramsi.msi" /passive'
!macroend:::tip[Is there a way to call just when the app is installed (or uninstalled) manually and not on update?]
Use ${isUpdated}.
${ifNot} ${isUpdated}
# your code
${endIf}:::
GUID vs Application Name
Windows requires to use registry keys (e.g. INSTALL/UNINSTALL info). Squirrel.Windows simply uses application name as key.
But it is not robust — Google can use key Google Chrome SxS, because it is a Google.
So, it is better to use GUID.
You are not forced to explicitly specify it — name-based UUID v5 will be generated from your appId or name.
It means that you should not change appId once your application in use (or name if appId was not set). Application product name (title) or description can be safely changed.
You can explicitly set guid using option nsis.guid, but it is not recommended — consider using appId.
It is also important to set the Application User Model ID (AUMID) to the appId of the application, in order for notifications on Windows 8/8.1 to function and for Window 10 notifications to display the app icon within the notifications by default. The AUMID should be set within the Main process and before any BrowserWindows have been opened, it is normally the first piece of code executed: app.setAppUserModelId(appId)
Portable
To build portable app, set target to portable (or pass --win portable).
For portable app, following environment variables are available:
PORTABLE_EXECUTABLE_FILE- path to the portable executable.PORTABLE_EXECUTABLE_DIR- directory where the portable executable is located.PORTABLE_EXECUTABLE_APP_FILENAME- sanitized app name to use in file paths.
The portable target also supports a custom NSIS script via portable.include (a single path or an array of paths). Unlike the installer targets, build/installer.nsh is not auto-discovered for portable builds — a custom script is only included when the option is explicitly set, so an installer.nsh written for the installer target does not silently leak into portable builds.
Common Questions
:::tip[How do I change the default installation directory?]
It is very specific requirement. Do not do if you are not sure. Add custom macro:
!macro preInit
SetRegView 64
WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\MyApp"
WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\MyApp"
SetRegView 32
WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\MyApp"
WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\MyApp"
!macroend:::
:::tip[Is it possible to make a single installer that will allow configuring user/machine installation?]
Yes, you need to switch to assisted installer (not default one-click).
package.json
"build": {
"nsis": {
"oneClick": false
}
}electron-builder.yml
nsis:
oneClick: false:::
Configuration
{!./app-builder-lib.Interface.NsisOptions.md!}
Pkg (`website/docs/pkg.md`)
The top-level pkg key contains a set of options instructing electron-builder on how it should build macOS PKG product archive installers.
When to Use PKG
PKG (.pkg) files are macOS installer packages that run Apple's native Installer application. Use PKG when:
- System-level installs are required — installing launch daemons, kernel extensions (or System Extensions on modern macOS), or files outside
/Applications - Custom install scripts are needed — pre/postinstall shell scripts run with root privileges during installation
- Enterprise managed deployment — PKG integrates with MDM systems and software management tools like Munki, Jamf, and SCCM for macOS
- Installer UI customization — displaying welcome screens, license agreements, readme files, and conclusion pages
For standard consumer app distribution, DMG is the conventional choice. For Mac App Store distribution, see MAS.
Certificate Requirements
Signing a PKG file requires a Developer ID Installer certificate — this is a different certificate from the Developer ID Application certificate used to sign the app bundle inside the PKG.
pkg:
identity: "Developer ID Installer: My Company (TEAM1234AB)"Or use the CSC_INSTALLER_LINK and CSC_INSTALLER_KEY_PASSWORD environment variables in CI.
:::tip
You can have both Developer ID Application and Developer ID Installer certificates. The application certificate signs the .app bundle, and the installer certificate signs the .pkg wrapper.
:::
:::note[v27: pkg.identity is not inherited]
As of v27 the .pkg installer identity resolves from pkg.identity (or CSC_INSTALLER_LINK) only — it no longer falls back to the app/mac-level signing identity (the old mac.identity fallback is gone, since that field moved to mac.sign.identity). If your .pkg relied on inheriting the mac-level identity, set pkg.identity explicitly.
:::
Installation Location
By default the app is installed to /Applications. Override with installLocation:
pkg:
installLocation: "/Applications"Setting a non-standard install location (e.g., /usr/local/bin) is unusual for Electron apps but may be needed for CLI tools bundled with the app.
Install Scripts
Place shell scripts in the scripts directory (default: build/pkg-scripts/). These run at install time with root privileges:
| Script Name | When It Runs |
|---|---|
preinstall |
Before files are installed |
postinstall |
After files are installed |
preupgrade |
Before an upgrade (rarely used) |
postupgrade |
After an upgrade (rarely used) |
pkg:
scripts: build/pkg-scripts # defaultExample postinstall script:
#!/bin/bash
# Register a launch daemon after install
launchctl load /Library/LaunchDaemons/com.mycompany.myapp.daemon.plist
exit 0:::warning[Script Permissions]
Scripts must be executable (chmod +x). Scripts run as root during installation — test thoroughly. A failing script (non-zero exit code) will abort the installation.
:::
See the macOS Kernel Extensions tutorial for a detailed example.
Installer UI Customization
Customize the pages displayed in Apple's Installer:
pkg:
welcome: build/welcome.html # or .rtf, .txt
license: build/license.html # or .rtf, .txt
conclusion: build/conclusion.htmlSupported file formats for each page: HTML, RTF, plain text.
Background image — displayed behind the installer pages (macOS 10.5+):
pkg:
background:
file: build/installer-background.png
alignment: center # center | left | right | top | bottom | topleft | topright | bottomleft | bottomright
scaling: tofit # tofit | proportional | noneInstallation Behavior Options
Control how the installer behaves when re-installing or upgrading:
| Option | Default | Description |
|---|---|---|
isRelocatable |
true |
Allow users to choose a different install location |
isVersionChecked |
true |
Block downgrades (PKG with older version won't install over newer) |
hasStrictIdentifier |
true |
Require matching bundle ID for upgrades |
overwriteAction |
upgrade |
upgrade (recommended) or update |
For most apps, leave these at their defaults.
Forcing App Closure Before Install
If your installer or upgrade requires the app to not be running:
pkg:
mustClose:
- com.mycompany.myapp
- com.mycompany.myapp.helperThe Installer will prompt the user to quit any running processes with matching bundle IDs before proceeding.
Extra Component Packages
Bundle additional .pkg files alongside the main component:
pkg:
extraPkgsDir: build/extra-pkgsAll .pkg files in this directory will be included as additional components in the installer. Useful for installing additional software alongside your app (e.g., a CLI tool, a system extension).
Complete Example
pkg:
identity: "Developer ID Installer: My Company (TEAM1234AB)"
installLocation: "/Applications"
scripts: build/pkg-scripts
welcome: build/welcome.html
license: build/license.rtf
conclusion: build/conclusion.html
background:
file: build/background.png
alignment: center
scaling: tofit
mustClose:
- com.mycompany.myapp
isRelocatable: true
overwriteAction: upgradeTroubleshooting
"The package is not signed" error: Ensure you have a valid Developer ID Installer certificate (not Application). Check the certificate identity string matches exactly.
Script fails silently: Run the PKG in verbose mode via Terminal: sudo installer -verbose -pkg YourApp.pkg -target /. Non-zero exit from a script aborts installation.
App not found after install: Verify installLocation is correct and the app bundle ends up at the expected path. Check Console.app for installer logs.
PKG won't overwrite existing version: The isVersionChecked: false option disables version checking, allowing downgrades.
Configuration
{!./app-builder-lib.Interface.PkgOptions.md!}
Programmatic Usage (`website/docs/programmatic-usage.md`)
title: "Programmatic Usage"
:::note
Node.js >=22.12.0 required. electron-builder v27 ships as native ESM. CJS require() continues to work on Node >=22.12 via Node's stable require(esm) support.
:::
The example below uses ESM syntax (recommended). A CJS equivalent is shown at the bottom.
/* Detailed source-code truncated for AI context efficiency. */CJS equivalent (Node >=22.12.0):
const { build, Platform } = require("electron-builder")
build({
targets: Platform.MAC.createTarget(),
config: { /* options */ },
})
.then((result) => console.log(JSON.stringify(result)))
.catch((error) => console.error(error))Publish (`website/docs/publish.md`)
{!./partials/_upgrading-from-v26.md!}
The publish key contains a set of options instructing electron-builder on how it should publish artifacts and build update info files for auto update.
String | Object | Array<Object | String> where Object it is Keygen, Generic Server, GitHub, S3, Spaces, R2 or Snap Store options. Order is important — first item will be used as a default auto-update server. Can be specified in the top-level configuration or any platform- (mac, linux, win) or target- (e.g. nsis) specific configuration.
Note that when using a generic server, you have to upload the built application and metadata files yourself.
Travis and AppVeyor support publishing artifacts. But it requires additional configuration for each CI and you need to configure what to publish.electron-builder makes publishing dead simple.
!!! important "Publishing Must Be Explicitly Requested"
Publishing is not performed automatically. You must explicitly request publishing using the --publish CLI flag (e.g., --publish always, --publish onTag, --publish onTagOrDraft) or by setting the publish option in your configuration.
If GH_TOKEN or GITHUB_TOKEN is defined — defaults to [{provider: "github"}].
If KEYGEN_TOKEN is defined and GH_TOKEN or GITHUB_TOKEN is not — defaults to [{provider: "keygen"}].
If GITHUB_RELEASE_TOKEN is defined, it will be used instead of (GH_TOKEN or GITHUB_TOKEN) to publish your release.
- e.g. mac:
export GITHUB_RELEASE_TOKEN=<my token> - the
GITHUB_TOKENwill still be used when your app checks for updates, etc. - you could make your
GITHUB_TOKEN"Read-only" when creating a fine-grained personal access token, and "Read and write" for theGITHUB_RELEASE_TOKEN. - "Contents" fine-grained permission was sufficient. (at time of writing - Apr 2024)
:::warning[Implicit publishing was removed in v27]
In v26 and earlier, electron-builder auto-detected when to publish based on CI environment conditions (npm lifecycle event, a git tag in CI, etc.). As of v27 this implicit behavior is removed — publishing never happens unless you request it. This closes a hole where unfinished work or secrets could be published unintentionally.
Explicitly specify your publish intent with the --publish CLI flag (e.g. --publish always, --publish onTag, --publish onTagOrDraft, --publish never) or set the publish option in your configuration. See v27 Breaking Changes → implicit --publish removed.
:::
:::info[Snap store]snap target by default publishes to the Snap Store (the app store for Linux). To force publishing to another provider, explicitly specify publish configuration for snap.
:::
You can publish to multiple providers. For example, to publish Windows artifacts to both GitHub and Bitbucket (order is important — first item will be used as a default auto-update server, so in this example the app will use GitHub as the auto-update provider):
{
"build": {
"win": {
"publish": ["github", "bitbucket"]
}
}
}win:
publish:
# an object provider for github with additional options
- provider: github
protocol: https
# a string provider for bitbucket that will use default options
- bitbucketWhich settings become the auto-update feed (`app-update.yml`) {#app-update-yml-feed}
The auto-update feed an installed app polls is the first provider written to app-update.yml inside the packaged app. One packaged app (for example win-unpacked, linux-unpacked or the .app bundle) holds a single app-update.yml, shared by every target built from it. It is resolved like this:
- The targets of that app that write update info — NSIS/NSIS web installer, AppX with
electronUpdaterAware, macOS dmg/zip, AppImage, deb/rpm/pacman — each resolve their ownpublish: the target-level value (e.g.nsis.publish) first, then the platform level (win.publish), then the top level. - If one or more of them emit a manifest under those settings, their first provider that receives the manifest becomes the feed — a provider with
publishAutoUpdate: falsegets nolatest*.yml, so it is skipped (withpublish: [{ provider: "s3", …, publishAutoUpdate: false }, "github"]the feed is GitHub). They must agree: two such targets whose first providers differ (for examplensis.publishpointing at S3 while an AppX inheritswin.publish: github, ordmg.publishdiffering fromzip.publish) would leave some installs polling a feed that never receives their manifest. A publishing build then fails at build start with anInvalidConfigurationError; a build without a publish policy logs a warning that publishing will fail, and writes noapp-update.ymlfor that app. Configurepublishonce at the platform level, or give the targets the same first provider (later providers may differ). Only what decides the feed is compared — the provider and options such asurl,bucket,region,path,owner,repoorchannel; options that only affect the upload (publishAutoUpdate,timeout, and for S3-compatible providersacl,storageClass,encryption) may differ. - If none of them emits a manifest — only non-updating targets such as snap or portable, or a target with
publish: nullorpublishAutoUpdate: falseon every provider — the platform/top-levelpublishis used, as before. - If no level configures
publishat all, the feed falls back to GitHub when therepositoryfield points there (see GitHub Repository).
For example, with only a target-level setting
nsis:
publish:
provider: s3
bucket: my-updatesinstalled NSIS apps poll the S3 bucket and embed the update-manifest trust key. Before this rule, the target-level setting was not read for app-update.yml: such a build shipped no app-update.yml at all (no feed and no trusted key), or — with a GitHub repository — a GitHub feed while the manifests went to S3.
You can also configure publishing using CLI arguments, for example, to force publishing snap not to Snap Store, but to GitHub: -c.snap.publish=github
A custom publish provider can be used if needed.
:::tip[Macros]
In all publish options File Macros are supported.
:::
How to Publish
Excerpt from CLI Usage of electron-builder command:
Publishing:
--publish, -p [choices: "onTag", "onTagOrDraft", "always", "never"]CLI --publish option values:
| Value | Description |
|---|---|
onTag |
on tag push only |
onTagOrDraft |
on tag push or if draft release exists |
always |
always publish |
never |
never publish |
But please consider using automatic rules instead of explicitly specifying publish:
If CI server detected, —
onTagOrDraft.If CI server reports that a tag was pushed, —
onTag.A release will be drafted (if one doesn't already exist) and artifacts published only if a tag was pushed.
If npm script named
release, —always.
Add to scripts in the development package.json:
"release": "electron-builder" and if you run yarn release, a release will be drafted (if doesn't already exist) and artifacts published.
Recommended GitHub Releases Workflow
Draft a new release. Set the "Tag version" to the value of
versionin your applicationpackage.json, and prefix it withv. "Release title" can be anything you want.For example, if your application
package.jsonversion is1.0, your draft's "Tag version" would bev1.0.Push some commits. Every CI build will update the artifacts attached to this draft.
Once you are done, publish the release. GitHub will tag the latest commit for you.
The benefit of this workflow is that it allows you to always have the latest artifacts, and the release can be published once it is ready.
Continuous Deployment Workflow on Amazon S3 and other non-GitHub
This example workflow is modeled on how releases are handled in Maven (it is an example of one of many possible workflows, you are not forced to follow it).
- Setup your CI to publish on each commit. E.g.
"dist": "electron-builder --publish always"in yourpackage.json. - Set your version in your application
package.jsonto1.9.0-snapshot(or1.9.0-masteror whatever you want your development channel to be named). This will publish a file namedsnapshot.ymland a build namedsomething-snapshot.exe(and corresponding for mac) to S3. - When you are ready to deploy, simply change your package version to
1.9.0and push. This will then produce alatest.ymlandsomething.exeon s3. Usually you'll git-tag this version as well (just to keep track of it). - Change the version back to a snapshot version right after, i.e.
1.10.0-snapshot, and commit it.
GitHub Repository {#github-repository-and-bintray-package}
Detected automatically using:
- repository in the application or development
package.json, - if not set, env
TRAVIS_REPO_SLUG- or
APPVEYOR_REPO_NAME - or
CIRCLE_PROJECT_USERNAME/CIRCLE_PROJECT_REPONAME,
- if no env, from
.git/configorigin url.
:::warning[The detected repository ships inside your app]
Whatever is detected here becomes the publish/update destination and, for auto-update-capable targets, is written to app-update.yml inside every packaged build - that is where electron-updater looks for updates for the lifetime of that install. A build reports the repository it inferred, including the source it came from - at info level when it comes from the repository field of package.json (deliberate configuration), at warn level when it is picked up from CI environment variables or .git/config:
• update feed inferred from repository info; it will be used as the publish/update destination
(written to app-update.yml in auto-update-capable targets) - specify it explicitly to be sure
it stays under your control reason=owner and repo not specified in the publish configuration
source=.git/config provider=github owner=my-org repo=my-appCheck that it is the repository you expect, and keep control of it. GitHub releases an owner or repository name for re-registration once it is renamed, transferred or deleted, so anyone who then claims that name can publish releases that already-installed copies of your app will download. Set owner and repo explicitly to avoid depending on detection at all:
{
"build": {
"publish": {
"provider": "github",
"owner": "my-org",
"repo": "my-app"
}
}
}:::
Publishers
Bitbucket
:::note[v27: authentication mode]
The Bitbucket publisher selects its auth scheme by whether a username is present:
- With a username (
bitbucket.username, or theBITBUCKET_USERNAMEenv var) → HTTP Basic auth. Use this for a Bitbucket app password or an Atlassian API token (the username is your Bitbucket username or Atlassian account email). - Without a username → the token is sent as
Authorization: Bearer <token>(a repository / project / workspace access token). This is new in v27; previously a token was always sent as Basic auth with the repo owner as the username.
If your CI sets BITBUCKET_TOKEN to an app password / API token without a username, set BITBUCKET_USERNAME too, or the request goes out as Bearer and fails authentication.
:::
{!./builder-util-runtime.Interface.BitbucketOptions.md!}
Github
:::note[v27: tagNamePrefix replaces vPrefixedTagName]
The GitHub vPrefixedTagName boolean was removed — use tagNamePrefix to control the tag prefix (defaults to "v"; set tagNamePrefix: "" for no prefix). electron-builder migrate-schema rewrites it. Note that v26 ignored an empty tagNamePrefix and still tagged v<version>; v27 honors it, so the migrator rewrites a v26 tagNamePrefix: "" to "v" (with a warning) to keep your existing tag names. (On GitLab, vPrefixedTagName is unchanged and still works.)
:::
{!./builder-util-runtime.Interface.GithubOptions.md!}
GitLab
{!./builder-util-runtime.Interface.GitlabOptions.md!}
Keygen
{!./builder-util-runtime.Interface.KeygenOptions.md!}
S3
{!./builder-util-runtime.Interface.S3Options.md!}
Snap Store
{!./builder-util-runtime.Interface.SnapStoreOptions.md!}
Spaces
{!./builder-util-runtime.Interface.SpacesOptions.md!}
R2
To publish to Cloudflare R2:
- Create an R2 API token with at least Object Read & Write permission on your bucket.
- Expose its credentials to electron-builder via the
CF_R2_ACCESS_KEY_IDandCF_R2_SECRET_ACCESS_KEYenvironment variables. - Enable public access for the bucket via an
r2.devsubdomain or a custom domain — R2's S3 API endpoint cannot serve unauthenticated downloads, soelectron-updatercannot download updates from it. - Set
publicUrlto that public base URL.
Auto-updating from R2 requires an electron-updater version that supports the r2 provider: electron-updater >= 7.0.0 (the release line containing this feature).
Note: files are uploaded with a single PUT request, and R2 limits single-PUT uploads to ~5 GiB per object.
{!./builder-util-runtime.Interface.R2Options.md!}
BYO Generic (create-your-own)
(And maybe submit it upstream in a PR!)
{!./builder-util-runtime.Interface.GenericServerOptions.md!}
Snap (`website/docs/snap.md`)
The top-level snapcraft key is the recommended way to configure Snap builds. It requires an explicit base field and separates per-core options cleanly.
The flat snap config key was removed in v27 — use snapcraft instead. electron-builder migrate-schema converts an old snap config to snapcraft automatically. See Migrating from snap to snapcraft if you are upgrading.
Core 24 (Recommended)
core24 targets Ubuntu 24.04 Noble and requires Electron 25.0.0+ (28.0.0+ recommended). It uses snapcraft v8 (craft-application framework) and brings first-class Wayland, the GNOME extension, and Launchpad remote builds for multi-arch CI.
{
"snapcraft": {
"base": "core24",
"core24": {
"confinement": "strict"
}
}
}Building Snap packages requires a Linux environment (or Docker).
Build Environments
Choose one build environment. They are mutually exclusive.
| Option | Platform | Recommended for |
|---|---|---|
useLXD: true |
Linux | Linux CI where nested virtualisation is unavailable |
useMultipass: true |
macOS / Windows / Linux | Local development |
useDestructiveMode: true |
Linux only | Docker CI containers without any virtualisation |
remoteBuild.enabled: true |
Any | Multi-arch CI (amd64, arm64, armhf) via Launchpad |
If none is set and you are on Linux, snapcraft's own default (Multipass) is used. On non-Linux platforms you must choose useMultipass or remoteBuild.
LXD
Container-based isolation. Preferred on most Linux CI systems because it does not require nested virtualisation (unlike Multipass).
{
"snapcraft": {
"base": "core24",
"core24": {
"useLXD": true
}
}
}LXD must be installed and the build user must be in the lxd group:
sudo snap install lxd
sudo usermod -aG lxd $USER
lxd init --minimalMultipass
VM-based isolation. The default choice for local development on macOS and Windows.
{
"snapcraft": {
"base": "core24",
"core24": {
"useMultipass": true
}
}
}Destructive Mode
Builds directly on the host without any VM or container (snapcraft --destructive-mode).
!!! warning
Not recommended for production builds. Destructive mode pollutes the host environment — any library present on the host at build time can end up in the snap, making builds difficult to reproduce. Prefer useLXD or useMultipass for clean builds.
Valid reason(s) to use this mode:
- CI test suites where the environment is already fully controlled.
The gnome extension is incompatible with destructive mode. electron-builder automatically clears extensions when useDestructiveMode is set; explicitly including "gnome" alongside it will throw an error.
{
"snapcraft": {
"base": "core24",
"core24": {
"useDestructiveMode": true,
"extensions": []
}
}
}Remote Build on Launchpad
Builds on Canonical's Launchpad infrastructure. Works from any platform and supports building for multiple architectures simultaneously (amd64, arm64, armhf).
{
"snapcraft": {
"base": "core24",
"core24": {
"remoteBuild": {
"enabled": true,
"buildFor": ["amd64", "arm64"],
"acceptPublicUpload": true
}
}
}
}!!! note
Unless privateProject is set, your source code is uploaded to a public Launchpad repository. Set acceptPublicUpload: true to suppress the interactive consent prompt in CI.
Authentication
Remote builds require Snapcraft Store credentials. electron-builder resolves them in this order:
remoteBuild.cscLink— base64-encoded credentials or a file path in the build config.SNAP_CSC_LINK— environment variable, same format (CI-recommended).SNAPCRAFT_STORE_CREDENTIALS— plain-text macaroon, read directly by snapcraft.- An active interactive
snapcraft loginsession.
Credentials are injected only into the spawned snapcraft subprocess environment and are never exposed through process.env.
This follows the same pattern as WIN_CSC_LINK / CSC_LINK for code signing.
CI Setup
Generate credentials once and store as a CI secret:
# Run locally — output is your CI secret value
snapcraft export-login - | base64 -w0Then set SNAP_CSC_LINK in your CI environment:
# GitHub Actions
- name: Build and publish snap
env:
SNAP_CSC_LINK: ${{ secrets.SNAP_CSC_LINK }}
run: npx electron-builder --linux snap:::warning[Classic Confinement]
Snap Store requires manual review and approval for classic confinement. Electron apps typically use strict with appropriate interface plugs.
:::
To reference credentials file directly via the config (useful for monorepos where the credential is managed per-project):
{
"snapcraft": {
"base": "core24",
"core24": {
"remoteBuild": {
"enabled": true,
"cscLink": "file://..."
}
}
}
}GNOME Extension
By default, core24 builds apply the gnome extension, which:
- Pulls in the
gnome-46-2404platform content snap (GTK3, GLib, etc.) - Adds
gtk-3-themes,icon-themes,sound-themescontent snaps - Adds the
gpu-2404GPU support content snap - Injects layout, environment variables, and plugs required by Electron on Ubuntu 24.04
You normally do not need to configure extensions at all. To opt out:
{
"snapcraft": {
"base": "core24",
"core24": {
"extensions": []
}
}
}When extensions is empty, electron-builder falls back to the standard Electron plug set and you are responsible for layout and content snap declarations.
:::tip[Minimal Plugs]
Start with only the plugs you actually need. Unnecessary plugs may slow Snap Store review. At minimum, browser-support, network, desktop, and desktop-legacy are needed for most Electron apps.
:::
Wayland
core24 always allows native Wayland. The snap runs with --ozone-platform=wayland on compositors that support it and falls back to XWayland otherwise.
To force X11-only mode, set forceX11: true:
{
"snapcraft": {
"base": "core24",
"core24": {
"forceX11": true
}
}
}Stage Packages
The default stage packages are ["libnspr4", "libnss3", "libxss1", "libappindicator3-1", "libsecret-1-0"].
Use the "default" keyword to extend the defaults rather than replace them:
{
"snapcraft": {
"base": "core24",
"core24": {
"stagePackages": ["default", "libv4l-0"]
}
}
}Core 22
core22 targets Ubuntu 22.04 Jammy and is supported via the legacy SnapCoreLegacy implementation. (As of v27, core24 is also stable — prefer it for new apps.)
{
"snapcraft": {
"base": "core22",
"core22": {
"summary": "My Electron app",
"confinement": "strict",
"stagePackages": ["default"]
}
}
}Key differences from core24:
allowNativeWaylanddefaults tofalse— Wayland is disabled by default for older Electron compatibility. Set totrueto enable. (The legacy interface usesallowNativeWayland; core24 usesforceX11.)- No extensions — no GNOME extension support; uses the
desktop-gtk2part instead. useTemplateApp— whenstagePackagesis not customised, electron-builder uses a pre-built Electron snap template for faster assembly (x64 and armv7l only).- Template builds skip
snapcraft— the image is packed directly with themksquashfsfrom thetoolsets.appimagebundle (non-template builds run thesnapcraftCLI).compression: "lzo"needs a toolset other than the legacy"0.0.0", whosemksquashfssupports onlygzipandxz.
Core 20
core20 targets Ubuntu 20.04 Focal.
{
"snapcraft": {
"base": "core20",
"core20": {
"summary": "My Electron app",
"confinement": "strict"
}
}
}Behaviour is identical to core22 above. Use core22 unless the Snap Store requires core20.
Core 18
core18 targets Ubuntu 18.04 Bionic. Use only if your store listing requires it.
{
"snapcraft": {
"base": "core18",
"core18": {
"summary": "My Electron app",
"confinement": "strict"
}
}
}Custom Pass-Through
Set base: "custom" to pass a snapcraft.yaml file (or an inline object) through to snapcraft verbatim. electron-builder performs no injection — no plugs, extensions, organize mappings, or desktop entries are added.
{
"snapcraft": {
"base": "custom",
"custom": {
"yaml": "build/snapcraft.yaml"
}
}
}The yaml path is resolved relative to the build resources directory (build/ by default). You can also supply the YAML content inline as an object in the config.
Migrating from `snap` to `snapcraft`
:::tip[Automated]electron-builder migrate-schema performs this restructuring for you. If the old config has no base, it assumes core20 and warns so you can confirm. See v27 Breaking Changes → snap → snapcraft.
:::
The removed snap key is equivalent to using snapcraft with a per-core options object. The base field moves to the top level of snapcraft, and all other fields move inside the corresponding core key.
// Before — deprecated snap key
{
"snap": {
"base": "core22",
"summary": "My app",
"confinement": "strict",
"stagePackages": ["default", "libfoo"]
}
}
// After — snapcraft key (recommended)
{
"snapcraft": {
"base": "core22",
"core22": {
"summary": "My app",
"confinement": "strict",
"stagePackages": ["default", "libfoo"]
}
}
}For core22 and older, place the per-core options inside the base-named sub-key (e.g. core22). Omit base from the inner object — it lives at snapcraft.base now.
The appPartStage option controls which files are included from the main app part:
snapcraft:
base: core22
core22:
appPartStage:
- -usr/share/doc
- -usr/share/manPublishing to the Snap Store
- Create an account at snapcraft.io, register your snap name, and install the
snapcraftCLI — see Publishing to the Snap Store - Configure publishing in your electron-builder config:
publish:
provider: snapStore
repo: myapp # your registered snap name
channels:
- stable- Build and publish:
electron-builder --linux snap --publish always
Channel Strategy
| Channel | Purpose |
|---|---|
edge |
Latest development builds (auto-published from CI) |
beta |
Beta testing builds |
candidate |
Release candidates |
stable |
Production releases |
Local Testing
Install a locally built snap without the Snap Store:
sudo snap install --dangerous dist/myapp_1.0.0_amd64.snapRun and check logs:
myapp
journalctl --user -xe | grep myappTo uninstall: sudo snap remove myapp
Debugging
Enable verbose electron-builder output:
DEBUG=electron-builder electron-builder --linux snapFor snap runtime issues, check confinement denials:
snap run --shell myapp
# Inside the snap shell, test what's accessibleConfiguration
`snapcraft` (new)
{!./app-builder-lib.Interface.SnapcraftOptions.md!}
`SnapOptions24` (core24)
{!./app-builder-lib.Interface.SnapOptions24.md!}
`RemoteBuildOptions`
{!./app-builder-lib.Interface.RemoteBuildOptions.md!}
`SnapOptionsCustom`
{!./app-builder-lib.Interface.SnapOptionsCustom.md!}
`SnapOptionsLegacy` (core18 / core20 / core22)
{!./app-builder-lib.Interface.SnapOptionsLegacy.md!}
Squirrel Windows (`website/docs/squirrel-windows.md`)
title: "Squirrel.Windows"
The top-level squirrelWindows key contains a set of options instructing electron-builder on how it should build Squirrel.Windows.
Squirrel.Windows target is maintained, but deprecated. Please use nsis instead.
To use Squirrel.Windows please install electron-builder-squirrel-windows dependency.
To build for Squirrel.Windows on macOS or Linux, please install mono (brew install mono / apt install mono-devel).
The Squirrel vendor binaries (Squirrel.exe, SyncReleases.exe, nuget.exe, …) are downloaded from the checksummed squirrel.windows bundle; rcedit comes from the winCodeSign bundle. Pin a version or supply your own bundle via toolsets.squirrel.
Your app must be able to handle Squirrel.Windows startup events that occur during install and uninstall. See electron-squirrel-startup.
Configuration
{!./app-builder-lib.Interface.SquirrelWindowsOptions.md!}
Targets (`website/docs/targets.md`)
Target Selection Guide
This page helps you choose the right packaging format for each platform. The right choice depends on your distribution method, target audience, and operational requirements.
Windows Targets
| Target | Format | Best For | Admin Required | Auto-Update |
|---|---|---|---|---|
nsis |
.exe installer |
Consumer apps (most common) | Optional (per-user mode) | electron-updater |
nsis-web |
.exe web installer |
Apps with large assets | Optional | electron-updater |
portable |
.exe no-install |
USB drives, no-install scenarios | No | Manual |
appx |
.appx |
Windows Store, enterprise MDM | Managed by Store | Store only |
msix |
.msix / .msixbundle / .msixupload |
Windows Store, modern deployment (successor to AppX) | Managed by Store | Store only |
msi |
.msi |
Enterprise deployment (SCCM/Intune/GPO) | Yes | Not supported via electron-updater |
msi-wrapped |
.msi wrapping NSIS |
Enterprise + existing NSIS installer | Yes | electron-updater |
squirrel.windows |
.exe / NuGet |
Legacy (not recommended) | No | Squirrel |
When to Use Each Windows Target
NSIS (nsis) — the default and best choice for most applications.
- Consumer software distributed via your website or GitHub releases
- Both per-user (no admin) and per-machine install modes available
- Script-based customization via NSIS macros (
.nshhooks) — not visual UI theming - Works with electron-updater for auto-updates
NSIS Web (nsis-web) — use when your app download is very large.
- Produces a small stub installer that downloads the full payload at install time
- Suitable for apps with many platform-specific binaries
- Requires internet connectivity at install time
Portable (portable) — use when no installation at all is the goal.
- Single executable, runs from any location
- No registry entries, no Start menu entries
- Common for developer tools, USB-stick deployments
- Auto-updates require manual implementation
AppX (appx) — use for Windows Store distribution or enterprise MDM.
- Required for listing in the Microsoft Store
- Enterprise IT can deploy via Intune/MDM without the Store
- Code signing with a trusted certificate is required for sideloading
- See AppX Configuration
MSIX (msix) — the modern successor to AppX (beta); prefer it for new Store/MDM work.
- Produces
.msix, multi-architecture.msixbundle, and Store.msixuploadartifacts - Adds modern manifest features: package integrity and Windows services
- Requires the modern
winCodeSigntoolset (the default; only the legacy0.0.0bundle is rejected); builds on Windows 10 / Windows Server 2012 R2 (6.3+) or later, or macOS via Parallels - See MSIX Configuration
MSI (msi) — use for enterprise deployment via Group Policy or SCCM.
- IT departments can deploy silently across managed machines
- Supports standard MSI command-line flags (
/quiet,/passive) - Upgrade code is critical — changing it breaks silent upgrades of existing installs
- See MSI Configuration
MSI-Wrapped (msi-wrapped) — use when you need MSI packaging but want NSIS features.
- Wraps your existing NSIS installer inside an MSI container
- Enables Group Policy deployment of apps with NSIS-based setup logic
- See MSI-Wrapped Configuration
Windows Decision Tree
Do you need Windows Store distribution?
→ Yes, modern (recommended): MSIX
→ Yes, legacy: AppX
Do you need enterprise GPO/SCCM/Intune deployment?
→ Yes, native MSI: MSI
→ Yes, but keep NSIS features: MSI-Wrapped
Is this a consumer app?
→ Normal installer: NSIS (default)
→ Very large download: NSIS-Web
→ No installation at all: PortablemacOS Targets
| Target | Format | Best For | Code Signing | Notarization |
|---|---|---|---|---|
dmg |
.dmg disk image |
Standard consumer distribution | Developer ID Application | Required (10.15+) |
pkg |
.pkg installer |
System-level installs, launch agents | Developer ID Installer | Required (10.15+) |
zip |
.zip archive |
Update server payload, minimal size | Developer ID Application | Required (10.15+) |
mas |
App bundle (Store) | Mac App Store | Apple Distribution | No (Apple signs) |
mas-dev |
App bundle (dev) | Testing MAS builds locally | Apple Development | No |
dir |
Directory | Development / debugging | Optional | No |
When to Use Each macOS Target
DMG (dmg) — the standard for consumer macOS distribution.
- Drag-and-drop install with
/Applicationsshortcut - Customizable window, background, icon layout
- electron-updater supports updates via ZIP payload
- See DMG Configuration
PKG (pkg) — use when you need system-level installation.
- Required for kernel extensions, launch daemons, or files outside
/Applications - Pre/post-install scripts via the
scriptsdirectory - Requires a separate "Developer ID Installer" certificate
- See PKG Configuration
ZIP — use as the update payload for electron-updater.
- Smaller than DMG for the same content
- Not user-facing — used internally by the auto-update mechanism
- Commonly built alongside DMG
MAS (mas) — required for Mac App Store submission.
- Uses App Sandbox — your app must declare all permissions via entitlements
- Requires provisioning profiles
- Apple handles signing during the Store submission process
- See MAS Configuration
macOS Decision Tree
Is the app going to the Mac App Store?
→ Yes: MAS
→ Testing MAS builds: mas-dev
Does the app install system components (daemons, kernel extensions)?
→ Yes: PKG
Standard consumer distribution?
→ Yes: DMG (+ ZIP for auto-updates)Linux Targets
| Target | Format | Best For | Sandboxed | Distro Support |
|---|---|---|---|---|
AppImage |
.AppImage |
Universal, no install | No | Universal |
deb |
.deb |
Debian, Ubuntu, Mint | No | Debian-based |
rpm |
.rpm |
Fedora, RHEL, openSUSE | No | RPM-based |
snap |
Snap package | Ubuntu-primary, Store distribution | Yes | Any (snapd) |
flatpak |
Flatpak bundle | Cross-distro sandboxed | Yes | Any (flatpak) |
pacman |
.pkg.tar.zst |
Arch Linux, Manjaro | No | Arch-based |
apk |
.apk |
Alpine Linux, Docker | No | Alpine |
freebsd |
.pkg |
FreeBSD | No | FreeBSD |
p5p |
IPS package | Solaris / illumos | No | Solaris-based |
zip, tar.gz |
Archive | CDN / custom deployment | No | Universal |
When to Use Each Linux Target
AppImage — built by default alongside Snap; best first choice for broad compatibility.
- Single file, no installation, no root required
- Runs on virtually any x86_64 Linux distribution
- AppImageLauncher handles desktop integration
- electron-updater supports delta updates via embedded blockmap (no separate file needed)
- See AppImage Configuration
DEB — use for Debian-based distribution (Ubuntu, Mint, etc.).
- Installable via
aptfrom a repository or directly - Best when targeting Ubuntu users who expect
.debpackages - See Linux Configuration → DEB
RPM — use for Red Hat-based distribution (Fedora, RHEL, CentOS, openSUSE).
- Installable via
dnf/yum - See Linux Configuration → RPM
Snap — use for Ubuntu-primary distribution via the Snap Store.
- Automatic updates via Snap Store
- Sandboxed with
strictconfinement - Required for Snap Store listing
- See Snap Configuration
Flatpak — use for cross-distro sandboxed distribution.
- Works on any distro with
flatpakinstalled - electron-builder produces single-file bundles (not Flathub repository packages)
- See Flatpak Configuration
Pacman — use for Arch Linux and Arch-based distributions.
- Beta status — test thoroughly
- See Linux Configuration → Pacman
APK — use for Alpine Linux or Docker container base images.
- Alpine uses musl libc; some native modules need recompilation
- See Linux Configuration → Alpine APK
Linux Decision Tree
Universal distribution, no install required?
→ AppImage (default)
Ubuntu Store / auto-update via Snap?
→ Snap
Cross-distro sandbox?
→ Flatpak
Debian/Ubuntu apt repository?
→ DEB
Fedora/RHEL/openSUSE dnf repository?
→ RPM
Arch Linux?
→ Pacman (beta)
Alpine / Docker?
→ APKCombining Multiple Targets
You can build multiple targets in a single run:
# electron-builder.yml
win:
target:
- nsis
- portable
mac:
target:
- dmg
- zip
linux:
target:
- AppImage
- deb# CLI: build all configured targets
electron-builder --mac --win --linux
# Build specific targets
electron-builder --win nsis --win msi
# Build for specific architectures
electron-builder --mac --x64 --arm64Related Pages
- Architecture & Multi-Arch — building for multiple CPU architectures
- GitHub Actions — CI/CD workflows for multi-platform builds
- Code Signing — signing requirements by platform
Toolsets (`website/docs/toolsets.md`)
title: "Toolsets"
electron-builder relies on a handful of external binary bundles — the NSIS compiler, Wine, the Windows code-signing tools, the AppImage runtime, FPM, and a few others — that it downloads on demand and caches locally. The top-level toolsets key lets you pin the version of each bundle, or point electron-builder at a bundle you host yourself.
For most projects you never touch this key: every toolset defaults to the newest published bundle. You reach for toolsets only to pin a specific (or legacy) version, or to supply a custom bundle in place of a built-in one.
:::info[New in v27]
Before v27, toolset selection was a mix of fixed version pins and environment-variable overrides (APPIMAGE_TOOLS_PATH, ELECTRON_BUILDER_NSIS_DIR, USE_SYSTEM_WINE, …). v27 removes every one of those env vars and replaces them with the typed toolsets config on this page. See Toolsets & environment variables in the breaking-changes reference and the Replacing removed environment variables section below.
:::
The toolsets
Each property of toolsets corresponds to one downloadable bundle, hosted at electron-userland/electron-builder-binaries:
| Toolset | Used for | "latest" resolves to |
|---|---|---|
winCodeSign |
Windows code signing & resource editing (signtool / osslsigncode, rcedit, Windows Kits for AppX/MSIX) |
1.3.0 |
appimage |
Building .AppImage files (mksquashfs, unsquashfs, the self-executing runtime); its mksquashfs also packs legacy-base snaps built from the snap template app |
1.1.0 |
nsis |
Compiling Windows installers (makensis, plugin DLLs, elevate.exe) |
1.2.1 |
wine |
Running Windows tools (NSIS, rcedit, signtool) on non-Windows hosts | system (host wine on PATH) |
fpm |
Building Linux packages (.deb, .rpm, .pacman, …) on macOS & Linux |
2.2.1 |
linuxToolsMac |
Building Linux targets / .tar.lz archives on macOS (ar, lzip, gtar) |
1.0.1 |
sevenZip |
Extracting .7z and .tar.xz archives internally |
1.0.1 |
icons |
Converting source images to .icns, .ico, and PNG icon sets |
1.2.3 |
squirrel |
Building Squirrel.Windows installers (Squirrel.exe, SyncReleases.exe, nuget.exe, 7z) — requires electron-builder-squirrel-windows |
1.1.1 |
:::note[Platform notes]
wineis only needed to build Windows targets on a non-Windows machine. On Windows it has no effect. It defaults to the host-installedwineonPATH("system") on both macOS and Linux, so a macOS host that builds Windows targets needs Wine installed (brew install --cask wine-stable).toolsets.wine: "1.0.1"still downloads the Wine 11.0 bundle on macOS when pinned explicitly, but the published bundle ships no PE builtins, so it cannot run Windows tools on its own.winCodeSignis used on all platforms (signtool.exeon Windows,osslsigncodeon macOS/Linux).squirrelis only used by thesquirrelWindowstarget. It runs natively on Windows; on macOS/Linux it needs a host-installedmono, andrcedit(fromwinCodeSign) runs under Wine.fpm,icons, andsquirreleach have only one published version today, so"latest"and the listed version are equivalent.
:::
For the full version-by-version breakdown of what each "latest" bundle upgrades from — and which are drop-in replacements versus behavior changes — see the migration table in Toolset defaults resolve to "latest".
Default resolution — `"latest"`
Every toolsets.* property defaults to "latest". An unset property and the literal string "latest" both resolve to the newest published bundle for that toolset. These two are interchangeable:
{ "build": { "toolsets": {} } } // unset → latest
{ "build": { "toolsets": { "nsis": "latest" } } } // explicit latest:::notenull is no longer accepted: it was dropped from the ToolsetConfig type and the configuration schema rejects it, so use "latest" or omit the key. electron-builder migrate-schema removes null entries (and rewrites the retired appimage: "1.0.2" pin to "1.0.3").
:::
Pinning to a concrete version is as simple as naming it:
toolsets:
nsis: "1.2.1"
winCodeSign: "1.3.0"Pinning the legacy bundle (`"0.0.0"`)
Every toolset accepts the sentinel version "0.0.0", which selects the pre-v27 legacy bundle for that tool. It is the escape hatch if a newer bundle introduces a regression:
{
"build": {
"toolsets": {
"winCodeSign": "0.0.0",
"nsis": "0.0.0",
"appimage": "0.0.0",
"wine": "0.0.0"
}
}
}Pinning "0.0.0" also changes a few behaviors that are gated on the bundle version — for example, the legacy AppImage bundle (appimage: "0.0.0") is the FUSE2 runtime and re-adds the automatic --no-sandbox launch argument, and a winCodeSign below 1.3.0 forces the legacy PowerShell path for Azure Trusted Signing (see Code signing & toolsets).
:::warning[Short-term workaround only]"0.0.0" is intended as a temporary fallback while you resolve an incompatibility. The alias may be removed in a future major release — prefer moving to a current bundle (or a custom toolset) rather than relying on it long-term.
:::
Custom toolsets
Instead of a version string, any toolset can be set to a ToolsetCustom object to supply your own bundle:
toolsets.<name>: { url: string, checksum?: string, version?: string }| Field | Required | Description |
|---|---|---|
url |
yes | An https:// URL or a file:// path. See below. |
checksum |
for downloaded archives | SHA-256 of the archive as 64 hex characters, or its SHA-512 as 88 base64 characters. See Custom toolset checksums. Required for https:// URLs and for file:// archive files. Not needed for a bare file:// directory (used as-is, no caching). |
version |
no | Label used only in the local cache directory name. Falls back to the first 8 hex characters of the checksum digest when omitted. |
url accepts two forms:
https://…— the bundle is downloaded, checksum-verified, extracted, and cached locally.file://…— a local path. A bare directory is used as-is (no checksum, no extraction). A local archive file is checksum-verified, then extracted and cached (checksum required); a mismatch fails the build and leaves the file untouched. Relativefile://paths must resolve inside the project's build-resources directory; absolute paths are used directly.
// Remote bundle (URL) — checksum required
{ "build": { "toolsets": { "nsis": {
"url": "https://example.com/my-nsis-bundle-1.0.tar.gz",
"checksum": "<SHA-256 hex or base64 SHA-512 of the archive>",
"version": "my-custom-1.0"
} } } }
// Local directory (used as-is, no checksum)
{ "build": { "toolsets": { "appimage": {
"url": "file:///path/to/my-appimage-tools-dir"
} } } }Custom toolset checksums {#custom-toolset-checksum}
Every custom toolset bundle accepts the same checksum formats, whichever toolset it replaces: winCodeSign, appimage, nsis, wine, fpm, linuxToolsMac, sevenZip, icons or squirrel.
- Formats: either
- the SHA-256 of the archive file as 64 hex characters (an uppercase value is lowercased for you), or
- the SHA-512 of the archive file as 88 base64 characters, ending in
==. v26 used this format for all toolset checksums, so a v26 value keeps working unchanged intoolsets.<name>.checksum.
- Why SHA-256 hex is the default: v27 downloads toolsets with the official
@electron/getpackage, whose checksum verification (sumchecker) only supports SHA-256 hex. electron-builder verifies a base64 SHA-512 itself. - Not accepted: prefixed forms such as
sha256:…orsha512-…, and a SHA-512 written as hex. electron-builder rejects a checksum in any other format before downloading or extracting anything. - When it is needed: for an
https://URL and for afile://archive file. A barefile://directory needs none and is not verified. - What is verified: a downloaded archive is verified before it is cached or extracted (a corrupted download is deleted), and an archive already in the electron-builder cache is re-verified before it is extracted again. A
file://archive is verified each time before it is extracted; on a mismatch the build fails and your file is left in place.
Compute it from the exact archive that url points to:
| Platform | SHA-256 (hex) | SHA-512 (base64) |
|---|---|---|
| macOS, Linux | shasum -a 256 my-bundle.tar.gz (or sha256sum my-bundle.tar.gz) |
openssl dgst -sha512 -binary my-bundle.tar.gz | openssl base64 -A |
| Windows (PowerShell) | (Get-FileHash -Algorithm SHA256 my-bundle.tar.gz).Hash |
[Convert]::ToBase64String([Security.Cryptography.SHA512]::Create().ComputeHash([IO.File]::ReadAllBytes((Resolve-Path my-bundle.tar.gz)))) |
| Windows (cmd) | certutil -hashfile my-bundle.tar.gz SHA256 |
— |
{ "build": { "toolsets": { "nsis": {
"url": "https://example.com/my-nsis-bundle-1.0.tar.gz",
"checksum": "56997fdefe25e7928a1a68b4583d08b240b66cf660234053b20131a74cc082f4",
"version": "my-custom-1.0"
} } } }Recompute the checksum whenever you rebuild or repack the archive.
:::warning[The bundle must mirror the built-in layout]
A custom bundle has to match the directory layout of the corresponding built-in bundle — electron-builder looks for the same executables in the same relative paths. Use the build scripts in electron-builder-binaries/packages as the reference for each toolset's expected structure.
:::
Supported archive formats
Archives supplied via url are extracted automatically. Supported formats: .zip, .7z, .tar.gz / .tgz, .tar.xz / .txz and .tar.7z. A bare directory (no archive) is used as-is.
The two families are unpacked differently, which decides where the bundle root is:
- Tar archives (
.tar.gz,.tgz,.tar.xz,.txz,.tar.7z) are extracted with their first path component stripped, so put everything inside one top-level folder (tar -czf bundle.tar.gz my-bundle). That folder becomes the bundle root. .zipand.7zare extracted as they are, so put the bundle's files at the root of the archive, not inside a folder. A release zip that wraps everything in a versioned folder (such asnsis-3.10/) has to be repacked.
:::note[sevenZip exception]
Because 7-Zip is the tool electron-builder uses to extract .7z and .tar.xz archives, a custom sevenZip bundle can't itself be one of those formats — that would be circular. Supply it only as a .tar.gz, .zip, or bare file:// directory. (The bundle must contain bin/7za on macOS/Linux or bin/7za.exe on Windows.)
:::
Custom NSIS bundle layout {#custom-nsis-bundle-layout}
:::warning[Did you use deprecated .customNsisBinary/.customNsisResources properties in v26 config?]
Checkout our v26-> v27 migration guide and relevant breaking changes note.
:::
Custom NSIS bundle Explanation
The new NSIS bundle is extremely flexible in-that it provides entrypoints to give you full control of how NSIS execution begins & where NSIS Resources are checked for. There is one bash script entry-point for linux/mac builds at /makensis in the bundle and two entrypoints for windows cmd/powershell builds respectively /makensis.cmd & /makensis.ps1. Any time makensis is invoked by electron-builder with a custom-nsis-toolset it passes off all the execution arguments to the relevant flavored script-shim based on the build-machine OS. These scripts also make it trivial to set the NSISDIR (NSIS Resources Directory) wherever you want.
The flexible nature of this toolset provides a greener-pasture for custom NSIS development to make some practical and exotic progress.
:::note[NSIS Entrypoint Shim Scripts]
If you do not need to support builds on Linux/MacOS then you can ommit the /makensis bash-script entrypoint from your bundle. Similarly if you do not need to support builds on Windows you can ommit the /makensis.cmd//makensis.ps1 entrypoint shim scripts from your bundle. At present /makensis.ps1 is not actually used by electron-builder - It is still considered a required file & not including it may cause your bundle to break unexpectedly from a future electron-builder update.
:::
Default Custom NSIS bundle Anatomy
A functional flushed out example NSIS Bundle can be found from electron-builder-binaries Default NSIS Bundle. It contains some resources not required for builds that we think still could be useful.
This is the same bundle we use for vanilla-builds and can be explored to get an idea of how custom-nsis bundles work. Here's a breakdown of some highlights:
| File Path | Purpose |
|---|---|
/makensis |
A bash script to route all builds running from macOS/Linux hosts to the appropriate makensis binary. Also sets NSISDIR (NSIS Resource Directory) to be /windows/ |
/makensis.cmd |
A command script to route incoming windows builds to the /windows/makensis.exe binary. Also sets NSISDIR (NSIS Resource Directory) to be /windows/ |
/makensis.ps1 |
A PowerShell script wich may be used in the future by electron-builder to route incoming windows builds from to the /windows/makensis.exe binary. Also sets NSISDIR (NSIS Resource Directory) to be /windows/ |
/elevate.exe |
A script used by 3rd party UAC plugin for privledge management. You must include this file in your bundle if you use ElectronBuilder's included NSIS scripts |
/windows/ |
This is the directory used as the NSIS dir for Custom NSIS Resources |
/windows/Plugins/x86-unicode/ |
This is where all Plugin DLLs called into by NSIS scripts live for this bundle. Some plugins ship with NSIS, some are 3rd party plugins. The 3rd party plugins found here will be required in your custom-build if you use ElectronBuilder's standard NSIS scripts. All makensis/makensis.exe binaries in this bundle were compiled with the x86-unicode target. They will only call DLLs from this directory. |
/windows/Contrib |
These files in here were used during the compilation of makensis/makensis.exe binaries in this bundle. They are not included at build-time but baked into the running binary. |
/windows/nsiconfig.nsh |
The global config file makensis processes automatically at startup, before your NSIS scripts. It sets default flags and defines like NSIS_MAX_STRLEN, NSIS_CONFIG_* options, etc. |
/mac/x64/makensis |
A native makensis binary compiled for Intel-CPU macintosh computers. Routed to by /makensis entrypoint |
/mac/arm64/makensis |
A native makensis binary compiled for Apple-Silicon macintosh computers. Routed to by /makensis entrypoint |
/linux/x64/makensis |
A native makensis binary compiled for x64 Linux computers. Routed to by /makensis entrypoint |
/linux/arm64/makensis |
A native makensis binary compiled for arm64 Linux computers. Routed to by /makensis entrypoint |
/windows/makensis.exe |
A native makensis.exe binary compiled to run on x64 Windows computers. Routed to by /makensis.cmd & /makensis.ps1 entrypoints |
All makensis/makensis.exe binaries in this bundle were compiled with the x86-unicode build Target.
Many files are omitted from this list, but most of the important ones are covered. See also NSIS Resources
The default bundle (nsis-bundle-3.12.tar.gz from the [email protected] release
Custom NSIS bundle Anatomy (minimal)
Here are the files that are absolutely required for a functional NSIS bundle. Note, you only have to provide shims for the operating system & architecture you intend to build on, you're also free to use wine if you have no native makensis binary. Hell go-wild on JIT compile custom NSIS binaries to bake in aesthetic installer choices at build time... Or you know w/e.
| File | Purpose |
|---|---|
/makensis |
A bash script to route all builds running from macOS/Linux hosts to the appropriate makensis binary. Also sets NSISDIR (NSIS Resource Directory) |
/makensis.cmd |
A command script used to route incoming windows builds to the appropriate makensis.exe binary. Also sets NSISDIR (NSIS Resource Directory) |
/makensis.ps1 |
A PowerShell script wich may be used in the future by electron-builder to route incoming windows builds to the appropriate makensis.exe binary. Also sets NSISDIR (NSIS Resource Directory). Technically it is unused now and you don't need to include it for a functional bundle. It is still considered required to prevent unexpected breaks in your custom-bundle build that are created in future electron-builder updates |
/elevate.exe |
A script used by 3rd party plugins for privledge management. You must include this file in your bundle if you use ElectronBuilder's included NSIS scripts |
/<NSISDIR>/Plugins/<Target>/* |
You must include the plugins provided by NSIS in your NSISDIR` (NSIS Resource Directory). If you use Electron Builder's built-in NSIS scripts. You also must provide DLLs for these required 3rd party plugins [INetC, StdUtils, SpiderBanner, NsProcess, UAC, WinShell, EmbedHTML, Nsisunz, NSISunzU] |
/<SomePath>/makensis || <SomePath>/makensis.exe |
You'll need at least one makensis/makensis.exe binary in your bundle and your entry script must pass on makensis execution to it. You only need to include entrypoint-scripts or binaries for the OS/Arch flavor your electron builder flow runs on. |
Note soft requirements of elevate.exe and 3rd party plugin DLLs only if you use electron-builder's NSIS scripts. In practice these are almost definitely required without a ton of NSIS scripting work to create entirely custom NSIS scripts for electron-builder.
You can grab these soft-required resources for your own custom bundle electron-builder-binaries Default NSIS Bundle
:::warning[Limited amd64-unicode support]
Tracking in issue electron-builder-binaries#230 there are no amd64-unicode compiled versions of soft-required 3rd party plugin DLLs. For best results with available NSIS resources, compile your custom NSIS binaries under the x86-unicode build Target.
:::
Out of the NSIS Bundle Anatomy
Worth a quick note that while the bundle itself provides the full and complete NSIS build tools. Electron-Builder and anything building with NSIS still passes in it's own NSIS scripts to use it.
Helpful Context
A custom toolsets.nsis bundle provides the whole NSIS toolset. The NSIS toolset is a build-tool used to compile installer/uninstaller executables for Windows build targets. To be frank, NSIS is an old tool which suffers from a complicated structure and a lot of nuance that is not cohesively documented in all areas. On the flip side NSIS is extremely battle-tested and enjoys one of the largest ecosystem of installer related plugins, tweaks and 3rd party tools. Notably it is a go-to installer type for supporting automatic-updates.
Since documentation can be a bit sparse with regards to the artifacts provided by NSIS and their interactions. A brief breakdown of some key things provided by NSIS are in-order.
NSIS Binary
As you'd expect, a functional NSIS toolset provides a binary makensis/makensis.exe for compiling installers/uninstaller executables for Windows build targets.
While NSIS is a windows build-target tool, it can be natively compiled to run on mac/windows/linux. On mac/linux it compiles to the makensis executable file and on windows it compiles to makensis.exe executable file.
Unlike modern compilers which allow for multiple output types from one compiler, the Target output type a makensis/makensis.exe binary compiles to is baked in at compile time. Valid NSIS Target types include x86-unicode,x86-ansi,amd64-unicode and one must be chosen when the tool is compiled.
A makensis/makensis.exe binary will only build outputs of the Target type it was compiled for. Also very important to note, a makensis/makensis.exe will only be compatible with resources built for the same Target type it was compiled for.
A makensis/makensis.exe must be compiled with NSIS_CONFIG_LOG=yes to make it possible to enable logging in NSIS scripts.
Many images and assets that effect the installer appearance are also baked into the makensis/makensis.exe binary when it is compiled. A custom compiled makensis/makensis.exe binary is required for certain aesthetic customizations to the install.
NSIS Resources
The makensis/makensis.exe binary can use a whole directory pattern of various resources for various parts of it's compilation execution. These resources are commonly arranged in a folder-tree like so:
| Resource Type | NSIS Directory Location | Description |
|---|---|---|
| Plugins | /Plugins/{ x86-unicode || x86-ansi || amd64-unicode }/<name>.dll |
Compiled binary DLLs to expose advanced execution to NSIS scripts. These must be compiled with the same Target as the makensis/makensis.exe binary |
| Include | /Include/<name>.nsh |
These are .nsh headers which can contain code/macros used by NSIS scripts |
| Conf File | /nsiconfig.nsh |
The global config file makensis processes automatically at startup, before your NSIS scripts. It sets default flags and defines like NSIS_MAX_STRLEN, NSIS_CONFIG_* options, etc. |
| Contrib | /Contrib/<various> |
These are language/image assets that are baked into the makensis/makensis.exe binary when it's created. They are not read during makensis exec. |
| Stubs | /Stubs/<name> |
Functional complete binaries that NSIS uses as a base to customize by appending byte-code to them. In newer versions of NSIS these must also match the makensis/makensis.exe binary Target flavor |
| Bin | /Bin/<name> |
Executables that can be used by plugins |
This is a non-exhaustive list of potential resources.
Plugins are particularly of-note here. NSIS scripts do not offer a lot of functionality on their own, so even very basic things can require a Plugin .dll to provide the functionality.
Debug NSIS logging
:::info[ You can toggle logging on an NSIS bundle via: ].installerDebugLogging However it additionally needs makensis/makensis.exe binaries and the stubs compiled with NSIS_CONFIG_LOG=yes, which the default bundle's NSIS versions are not.
:::
Replacing removed environment variables
v27 removes the toolset environment-variable overrides. Replace each with a toolsets.<name> custom object:
| Removed env var | Toolset it controlled | Replacement |
|---|---|---|
APPIMAGE_TOOLS_PATH |
AppImage build tools | toolsets.appimage: { url } |
LINUX_TOOLS_MAC_PATH |
linux-tools-mac bundle | toolsets.linuxToolsMac: { url } |
CUSTOM_FPM_PATH |
FPM executable | toolsets.fpm: { url } |
ELECTRON_BUILDER_NSIS_DIR |
NSIS compiler bundle | toolsets.nsis: { url } |
ELECTRON_BUILDER_NSIS_RESOURCES_DIR |
NSIS resources/plugins | toolsets.nsis: { url } |
CUSTOM_NSIS_RESOURCES |
Alternate NSIS resources | toolsets.nsis: { url } |
ELECTRON_BUILDER_WINE_TOOLSET_DIR |
Wine bundle | toolsets.wine: { url } |
USE_SYSTEM_WINE |
Host Wine instead of the bundle | toolsets.wine: "system" |
USE_SYSTEM_SIGNCODE |
Host signtool/signcode |
Configure via win.sign + winCodeSign |
USE_SYSTEM_OSSLSIGNCODE |
Host osslsigncode |
Configure via win.sign + winCodeSign |
USE_SYSTEM_FPM |
Host fpm instead of the bundle |
toolsets.fpm: { url } |
{ "build": { "toolsets": { "nsis": {
"url": "https://example.com/my-nsis-bundle.tar.gz",
"checksum": "<SHA-256 hex or base64 SHA-512 of the archive>"
} } } }:::warning[No env-var replacement for the signing overrides]
The two signing USE_SYSTEM_* variables (USE_SYSTEM_SIGNCODE, USE_SYSTEM_OSSLSIGNCODE) have no env-var equivalent — configure signing through win.sign and the winCodeSign toolset instead. USE_SYSTEM_WINE is replaced by the config value toolsets.wine: "system".
USE_SYSTEM_FPM is likewise removed. On Windows there is no bundled FPM, so an FPM-based target now requires an explicit custom toolsets.fpm ({ url: "file:///path/to/dir" }) and otherwise throws a clear configuration error — previously it silently fell back to a host fpm on PATH.
:::
See Toolset env-var overrides removed for the complete rationale.
Code signing and toolsets
Windows signing is driven by the winCodeSign toolset in combination with win.sign:
- The default
winCodeSign("latest"→1.3.0) bundles a modernsigntool/osslsigncode(native arm64) plus the Windows Kits used for AppX/MSIX. Thehsmandpkcs11signing modes require this modern bundle. - Azure Trusted Signing (
win.sign: { type: "azure" }) uses the fastersigntool /dlibpath out of the box, because the defaultwinCodeSignships the ATSdlib+ .NET 8 payload — no pin needed. To force the legacy PowerShellInvoke-TrustedSigningpath, pinwinCodeSignbelow1.3.0(e.g."1.2.1"or"0.0.0"). AToolsetCustomobject uses thedlibfrom your supplied bundle.
For full setup — certificate methods, HSM/PKCS#11, and Azure — see Code Signing for Windows and Azure Trusted Signing signtool /dlib is the default.
Troubleshooting (`website/docs/troubleshooting.md`)
Troubleshooting
This page covers common errors by category. Enable verbose logging to get more detail:
DEBUG=electron-builder electron-builder buildCode Signing
macOS
"No identity found for code signing"
: No Developer ID certificate is installed. Run security find-identity -v -p codesigning to list available identities. On CI, verify CSC_LINK is set and contains a valid base64-encoded .p12.
"CSSMERR_TP_CERT_REVOKED"
: Your certificate was revoked by Apple. Renew it in the Apple Developer portal.
"The certificate has expired"
: Renew the certificate in Apple Developer portal, re-export it, and update your CI secrets.
Build produces unsigned output without an error
: CSC_LINK isn't set, so electron-builder silently skips signing. Set forceCodeSigning: true to turn this into a build failure so you catch it early.
"Keychain not unlocked" / codesign hangs
: Verify CSC_LINK and CSC_KEY_PASSWORD are set correctly — electron-builder creates a temporary keychain and runs all required setup commands, including set-key-partition-list, automatically. If using a manually managed keychain (via CSC_KEYCHAIN), ensure you run security set-key-partition-list -S apple-tool:,apple: -s -k <keychain> after importing.
"App cannot be opened because the developer cannot be verified" (Gatekeeper)
: The app is unsigned, signed with the wrong certificate type (e.g., Mac App Store cert used for direct distribution), or not notarized. Check the certificate type and notarization status with spctl --assess --verbose dist/mac/MyApp.app.
macOS Notarization
"Package Invalid: the binary is not signed with a valid Developer ID certificate"
: You used an Apple Distribution / Mac App Store certificate. Notarization for direct distribution requires a Developer ID Application certificate.
"The executable does not have the Hardened Runtime enabled"
: Add hardenedRuntime: true to your mac.sign config. Also ensure entitlements include com.apple.security.cs.allow-jit.
"Notarization failed: invalid credentials"
: The APPLE_ID / APPLE_APP_SPECIFIC_PASSWORD combination is wrong, or the app-specific password was revoked. Regenerate it at appleid.apple.com.
"Notarization timed out"
: Apple's notarization service may be slow. Run again later. Check Apple System Status.
Windows
SmartScreen warning at install time
: Normal for standard OV certificates — trust builds over time based on download count. EV certificates bypass this. No fix needed; warn users in your release notes that the warning is expected.
"Publisher does not match" (AppX)
: The appx.publisher value in your config must exactly match the Subject field in your certificate. Copy it from the certificate's Properties → Details → Subject.
"SignTool error: No certificates were found that met all the given criteria"
: The certificate isn't in the certificate store, or WIN_CSC_LINK and WIN_CSC_KEY_PASSWORD are wrong. Verify the base64 decodes to a valid .pfx and that the password is correct.
ASAR and File Packaging
Native module crashes at runtime: "Error: Module did not self-register"
: The native module (.node file) is inside the ASAR archive, but native modules can't load from inside ASAR. Add to asar.unpack:
asar:
unpack:
- "node_modules/your-module/**"
- "**/*.node""ENOENT: no such file or directory" at runtime
: A file that exists in development is missing from the packaged app. Check if it's being excluded by a files pattern or by the default exclusions. Build with DEBUG=electron-builder and look for "excluding" log lines. Also check asar.unpack — files inside ASAR must be accessed via app.getAppPath(), not via __dirname relative paths.
App directory is too large
: Profile the ASAR after building:
npx asar list dist/mac/MyApp.app/Contents/Resources/app.asar | sortCommon culprits: TypeScript source files (.ts), source maps (.map), test fixtures, large binary assets. Add exclusion patterns to files:
files:
- "**/*"
- "!src/**"
- "!**/*.ts"
- "!**/*.map"
- "!**/__tests__/**""Cannot find module" at runtime after packaging
: A production dependency isn't being included. Check that it's in dependencies (not devDependencies) in package.json. electron-builder only includes production dependencies.
FileSet from path not found
: In files, the from path is relative to the app directory. In extraResources / extraFiles, from is relative to the project root. These are different when directories.app is set.
Native Modules
"Error: The module was compiled against a different Node.js version"
: electron-builder automatically rebuilds native modules for the target Electron version during the build. If you see this error at runtime during development (not during a build), ensure you are running your app via electron . from the project root, not from a globally installed Electron binary compiled against a different version.
Native module works in development but crashes after packaging
: The module likely needs to be in asar.unpack. Some modules also need their full directory tree unpacked:
asar:
unpack:
- "node_modules/better-sqlite3/**"
- "node_modules/sharp/**"Auto-Update
Updates not detected: app reports "up to date" when a new version exists
: Check that the publish configuration matches your distribution channel. For GitHub, ensure the release isn't a draft (draft releases are invisible to electron-updater). Verify app.getVersion() matches the version in package.json at build time.
"Cannot find latest.yml" / update server 404
: electron-builder generates latest.yml (Windows), latest-mac.yml (macOS), or latest-linux.yml only when publishing. Ensure you ran --publish always (or --publish onTagOrDraft) when building the release.
"sha512 checksum mismatch"
: The downloaded update file is corrupt or the wrong file is being served. Verify that the artifact and the .yml metadata file were generated together in the same build and not mixed from different builds. As of electron-builder 27, the manifest sha512 should be base64-encoded — a 128-hex-character "expected" value in the error message is the tell that a hand-rolled manifest still uses hex, which is accepted but deprecated in v27 and will be removed in v28; convert it with sha512sum file.ext | cut -d' ' -f1 | xxd -r -p | base64 -w0.
Delta updates (AppImage / NSIS-Web) fail
: For AppImage delta updates, electron-builder embeds a blockmap directly in the AppImage binary — no separate file needs to be published. If delta updates aren't working, verify the AppImage was built with electron-builder and that a publish provider is configured. For NSIS differential packages, nsis.differentialPackage must not be false (any other value — true, "compressed", "store-asar", or unset — builds a differential-aware package; "store-asar" additionally stores resources/app.asar uncompressed so app-code deltas stay proportional to what changed).
Build Environment
"Cannot find module 'electron'"
: electron must be in devDependencies, not dependencies. electron-builder expects to find it in the project's devDependencies to determine the Electron version to bundle.
"Resource does not exist" when downloading Electron
: The Electron version specified in your devDependencies doesn't exist for the target architecture. Check Electron releases to confirm the version and architecture combination exists.
Build hangs with no output
: Often caused by a native module rebuild that's hanging. Run with DEBUG=electron-builder and check the last log line. Also check if a spawned process is waiting for input (some postinstall scripts do this).
Out-of-memory error during packaging
: Large apps can exhaust memory during ASAR creation. Set NODE_OPTIONS=--max-old-space-size=4096 before running electron-builder.
Build fails in an offline / air-gapped environment
: electron-builder downloads build toolsets (e.g. WinCodeSign, AppImage tools) the first time they are needed, then caches the archive at <ELECTRON_BUILDER_CACHE>/<releaseName>/<filename>. On every subsequent build it reads from that cache and makes no network request.
For a first-time setup in a fully air-gapped environment, run the build once on a machine with internet access so the cache is populated, then copy the entire ELECTRON_BUILDER_CACHE directory to the air-gapped machine. Point both machines at the same directory with ELECTRON_BUILDER_CACHE=/path/to/shared/cache.
"Downloading" progress appears for an artifact that should be cached
: The toolset archive cache lives inside ELECTRON_BUILDER_CACHE (default: ~/Library/Caches/electron-builder on macOS, %LOCALAPPDATA%/electron-builder/Cache on Windows, ~/.cache/electron-builder on Linux). If this directory is cleared or not persisted between builds (common on ephemeral CI runners), the archive is re-downloaded. Persist the cache directory across CI runs to avoid repeated downloads.
Linux-Specific
"fpm not found"
: The DEB, RPM, Pacman, APK, FreeBSD, and P5P targets use FPM internally. electron-builder bundles FPM — if it fails to find it, try reinstalling electron-builder or clearing ~/.cache/electron-builder.
AppImage won't run: "FUSE not found" / "fusermount not found"
: As of v27 the default AppImage runtime is static and needs no host FUSE, so this affects only builds pinned to the legacy runtime (toolsets.appimage: "0.0.0"). Rebuild on the default runtime (remove the 0.0.0 pin), or install FUSE2 on the host:
# Ubuntu/Debian
sudo apt install fuse libfuse2
# Fedora
sudo dnf install fuse fuse-libsOr run with --appimage-extract-and-run:
./MyApp.AppImage --appimage-extract-and-runAppImage won't run: "squashfs: FATAL ERROR aborting"
: This is a symptom of the legacy FUSE2 runtime. Remove any toolsets.appimage: "0.0.0" pin so the build uses the default static runtime.
Flatpak build fails: "runtime not found"
: Install the Flatpak runtime before building:
flatpak install flathub org.freedesktop.Platform//24.08
flatpak install flathub org.freedesktop.Sdk//24.08Snap confinement issues: permission denied at runtime
: The snap is trying to access something not declared in its interface plugs. Run snap run --shell myapp to get a shell inside the snap, then test access. Add the appropriate plug to snap.plugs in your config.
macOS-Specific
DMG window layout doesn't look right
: DMG window positions are set by the macOS system and can behave unexpectedly if the dimensions in dmg.window.size don't match the contents positions. Ensure icon positions fit within the window bounds with some padding. Test by opening the built DMG manually.
"App is damaged and can't be opened"
: Usually means the app was downloaded without Gatekeeper quarantine being cleared, and notarization failed or wasn't performed. Run xattr -dr com.apple.quarantine /path/to/app temporarily during testing. Ensure notarization succeeds before distributing.
PKG installer fails: "The package … is not compatible with this version of macOS"
: Check pkg.minVersion matches your actual minimum supported macOS version.
"The application cannot be opened" on Apple Silicon
: The app may be x64-only but running on an ARM Mac without Rosetta. Either build a universal binary or ensure Rosetta 2 is installed (softwareupdate --install-rosetta).
Windows-Specific
NSIS installer: "The installer you are trying to use is corrupt or incomplete"
: The installer binary is corrupted. This can happen when the file is scanned/modified by antivirus during download or build. Check your AV exclusions during builds.
"The publisher could not be verified" (Windows SmartScreen)
: The app isn't code-signed, or signed with an untrusted certificate. See Windows Code Signing.
MSI: upgrades don't remove the old version
: The upgradeCode was changed between versions. The upgradeCode (a GUID) must remain identical across all versions of your application. See MSI Configuration.
AppX installation fails: "The package could not be installed"
: Common causes: certificate not trusted on the target machine; publisher in config doesn't match certificate Subject; Windows version below the minVersion requirement. Check Event Viewer → Applications and Services Logs → Microsoft → Windows → AppxPackagingOM for detailed error codes.
Docker / Cross-Platform Builds
Linux build on macOS/Windows hangs or fails
: Use the official Docker image:
docker run --rm -ti \
-v ${PWD}:/project \
-v ~/.cache/electron:/root/.cache/electron \
-v ~/.cache/electron-builder:/root/.cache/electron-builder \
electronuserland/builder:wine \
/bin/bash -c "npm ci && npx electron-builder --linux""wine: command not found" when building Windows on Linux
: Use the electronuserland/builder:wine Docker image which includes Wine. Or install Wine on the host system.
Getting More Help
- Run
DEBUG=electron-builder electron-builder build 2>&1 | tee build.logto capture full debug output - Search GitHub Issues
- When filing a bug report, use electron/minimal-repro to create a minimal reproduction case — issues with a repro get resolved significantly faster
Win (`website/docs/win.md`)
title: "Windows"
{!./partials/_upgrading-from-v26.md!}
The top-level win key contains a set of options instructing electron-builder on how it should build Windows targets. These options are applicable to any Windows target.
Common Questions
How do you delegate code signing?
In v27, all Windows signing is configured through the single win.sign discriminated union (type: "signtool" | "hsm" | "pkcs11" | "azure"). To delegate signing to a custom function, set the sign field on that union — it works for the signtool, hsm and pkcs11 types. See the Windows Code Signing guide and why sign.js is called 8 times.
"win": {
"sign": {
"type": "signtool",
"sign": "./customSign.js",
"publisherName": "CN=My Company, O=My Company, C=US"
}
}electron-builder cannot read the certificate your hook signs with, so set publisherName to that certificate's subject: it is written to app-update.yml, and electron-updater checks every downloaded update against it. It is never derived from a certificate for a hook, not even one in the config, so without it a build that writes app-update.yml (an nsis, nsis-web or electronUpdaterAware appx target with a publish configuration, including one inferred from a GitHub repository) fails. Set win.verifyUpdateCodeSignature: false instead only if your updates are not Authenticode-signed or you don't use electron-updater.
Copy the subject from a binary your hook has already signed (a previous release, or win-unpacked from an electron-builder --win dir build, which does not write app-update.yml) instead of typing it by hand. electron-builder cannot check it against the certificate the hook uses, and every component you list (CN, O, C, …) must match the certificate exactly — otherwise installed apps reject every later update:
- Windows (PowerShell):
(Get-AuthenticodeSignature .\dist\win-unpacked\<App>.exe).SignerCertificate.Subjectprints it in the form to use as-is. - macOS / Linux:
osslsigncode verify -in <App>.exe— theSubject:line under "Signer's certificate", printed as/C=US/ST=California/O=My Company, Inc./CN=My Company, Inc.. Keep onlyCN,OandCand write them asCN="My Company, Inc.", O="My Company, Inc.", C=US: OpenSSL names some other components differently from Windows (STinstead ofS, for example), and a value that contains a comma must be wrapped in double quotes.
:::note[Upgrading from v26]
The v26 win.signtoolOptions / win.azureSignOptions keys were removed — electron-builder migrate-schema rewrites them to win.sign automatically. See v27 Breaking Changes → Windows signing.
:::
File customSign.js in the project root directory:
exports.default = async function(configuration) {
// your custom code
}How do you use a custom verify function to enable nsis signature verification alternatives instead of powershell?
Use the NsisUpdater.verifyUpdateFileAuthenticodeSignature interface:
export type VerifyUpdateFileResult =
| { response: "success" }
| { response: "failure"; message: string }
export type VerifyUpdateFileAuthenticodeSignature = (
publisherName: string[],
path: string
) => Promise<VerifyUpdateFileResult>Pass a custom verify function to the nsis updater. For example, if you want to use a native verify function, you can use win-verify-signature.
import { NsisUpdater } from "electron-updater"
import { verifySignatureByPublishName } from "win-verify-signature"
export default class AppUpdater {
constructor() {
const options = {
requestHeaders: {
// Any request headers to include here
},
provider: 'generic',
url: 'https://example.com/auto-updates'
}
const autoUpdater = new NsisUpdater(options)
autoUpdater.verifyUpdateFileAuthenticodeSignature = async (publisherName: string[], path: string) => {
const result = verifySignatureByPublishName(path, publisherName)
return result.signed ? { response: "success" } : { response: "failure", message: result.message }
}
autoUpdater.addAuthHeader(`Bearer ${token}`)
autoUpdater.checkForUpdatesAndNotify()
}
}The built-in default uses windowsExecutableCodeSignatureVerifier.
The older property verifyUpdateCodeSignature, which differs only in name and return interface, is deprecated and kept only as a compatibility shim (shall be removed in electron-builder v28). The protected _verifyUpdateCodeSignature member is likewise deprecated in favour of _verifyUpdateFileAuthenticodeSignature.
How do you create a Parallels Windows 10 Virtual Machine?
:::warning[Disable "Share Mac user folders with Windows"]
If you use Parallels, you must not use "Share Mac user folders with Windows" feature and must not run installers from such folders.
:::
You don't need to have a Windows 10 license. A free license is provided (expires after 90 days, but this is not a problem because no additional setup is required).
- Open Parallels Desktop.
- File -> New.
- Select "Modern.IE" in the "Free Systems".
- Continue, Continue, Accept software license agreement.
- Select "Microsoft Edge on Windows 10".
- The next steps are general, see Installing Windows on your Mac using Parallels Desktop from "Step 6: Specify a name and location".
Parallels Windows 10 VM will be used automatically to build AppX on macOS. No need even start VM — it will be started automatically on demand and suspended after build. No need to specify VM — it will be detected automatically (first Windows 10 VM will be used).
How do you create a VirtualBox Windows 10 Virtual Machine?
If you are not on macOS or don't want to buy Parallels Desktop, you can use free VirtualBox.
- Download a Windows evaluation ISO from the Microsoft Evaluation Center (Microsoft no longer publishes the prebuilt "MSEdge on Win10" virtual machines).
- Create a new virtual machine in VirtualBox and install Windows from the ISO.
VirtualBox is not supported by electron-builder for now, so you need to set up the build environment on Windows if you want to use VirtualBox to build AppX (and other Windows-only tasks).
Configuration
{!./app-builder-lib.Interface.WindowsConfiguration.md!}
Features Auto Update (`website/docs/features/auto-update.md`)
title: "Auto Update"
Auto updates are enabled by the electron-updater package. Ideally, auto updates are configured to run in a CI pipeline to automatically provision new releases. See publish configuration for information on how to configure your local or CI environment for automated deployments.
Auto updates work as follows:
- You configure the package to build release metadata (
latest.yml) - Electron builder uploads the actual release targets and metadata files to the configured target (except for generic server, where you have to upload manually)
- You configure the Electron application to use auto-updates, which queries the publish server for possible new releases
Read the remainder of this guide to configure everything.
:::info[Code signing is required on macOS]
macOS application must be signed in order for auto updating to work.
:::
Auto-updatable Targets
- macOS: DMG.
- Linux: AppImage, DEB, Pacman and RPM.
- Windows: NSIS.
All these targets are default, custom configuration is not required. (Though it is possible to pass in additional configuration, e.g. request headers.)
:::info
- Squirrel.Windows is not supported. Simplified auto-update is supported on Windows if you use the default NSIS target, but is not supported for Squirrel.Windows.
You can easily migrate to NSIS. ziptarget for macOS is required for Squirrel.Mac, otherwiselatest-mac.ymlcannot be created, which causesautoUpdatererror. Default target for macOS isdmg+zip, so there is no need to explicitly specify target.
:::
Differences between electron-updater and built-in autoUpdater
The electron-updater package offers a different functionality compared to Electron's built-in auto-updater. Here are the differences:
- Linux is supported (not only macOS and Windows).
- Code signature validation not only on macOS, but also on Windows.
- All required metadata files and artifacts are produced and published automatically.
- Download progress and staged rollouts supported on all platforms.
- Different providers supported out of the box: (GitHub Releases, Amazon S3, DigitalOcean Spaces, Cloudflare R2, Keygen and generic HTTP(s) server).
- You need only 2 lines of code to make it work.
Quick Setup Guide
Install electron-updater as an app dependency.
Configure the
publishoptions depending on where you want to host your release files.Build your application and check that the build directory contains the metadata
.ymlfiles next to the built application. For most publish targets, the building step will also upload the files, except for the generic server option, where you have to upload your built releases and metadata manually.Use
autoUpdaterfromelectron-updaterinstead ofelectron:CommonJS
jsconst { autoUpdater } = require("electron-updater")ESM
jsimport { autoUpdater } from "electron-updater"TypeScript
typescriptimport electronUpdater, { type AppUpdater } from 'electron-updater'; export function getAutoUpdater(): AppUpdater { // Using destructuring to access autoUpdater due to the CommonJS module of 'electron-updater'. // It is a workaround for ESM compatibility issues, see https://github.com/electron-userland/electron-builder/issues/7976. const { autoUpdater } = electronUpdater; return autoUpdater; }Call
autoUpdater.checkForUpdatesAndNotify(). Or, if you need custom behaviour, implementelectron-updaterevents, check examples below.
:::note
Do not call setFeedURL. electron-builder automatically creates app-update.yml file for you on build in the resources (this file is internal, you don't need to be aware of it). Its feed is the first provider that receives the update manifest (publishAutoUpdate not false) in the publish settings of the targets that write update info — a target-level publish such as nsis.publish counts — see which settings become the auto-update feed.
:::
Examples
:::note[Example in TypeScript using system notifications]
import { autoUpdater } from "electron-updater"
export default class AppUpdater {
constructor() {
const log = require("electron-log")
log.transports.file.level = "debug"
autoUpdater.logger = log
autoUpdater.checkForUpdatesAndNotify()
}
}:::
- A complete example showing how to use.
- An encapsulated manual update via menu.
Custom Options instantiating updater Directly
If you want more control over the updater configuration (e.g. request header for authorization purposes), you can instantiate the updater directly.
import { NsisUpdater } from "electron-updater"
// Or MacUpdater, AppImageUpdater
export default class AppUpdater {
constructor() {
const options = {
requestHeaders: {
// Any request headers to include here
},
provider: 'generic',
url: 'https://example.com/auto-updates'
}
const autoUpdater = new NsisUpdater(options)
autoUpdater.addAuthHeader(`Bearer ${token}`)
autoUpdater.checkForUpdatesAndNotify()
}
}:::note[Credentials are only sent to the update feed's origin]
The credential headers in requestHeaders / addAuthHeader — headers such as Authorization, the same set that is removed on a cross-origin redirect — and the query string of the feed url are only sent to the feed's origin (scheme, host and port; as on redirects, an http → https upgrade of the feed host on the default ports keeps the headers and the feed query). With the generic, s3, spaces, r2, keygen, bitbucket, github and gitlab providers, an update file whose URL in latest*.yml is on another origin, and the blockmaps and differential range requests derived from it, are downloaded without them; such a URL keeps its own query string. A blockmap is downloaded from ${url}.blockmap with the file URL's query string, so a pre-signed installer URL's signature does not fit it: set files[].blockMapUrl in latest*.yml to the blockmap's own (pre-signed) URL. A relative value resolves like url (against the feed URL); an absolute URL is used as-is, with its own host and query string. Either follows the same origin rule. With a blockMapUrl, the old version's blockmap comes from the local cache or from previousBlockmapBaseUrlOverride; when neither has it, that one update is downloaded in full, which fills the cache for the next one (the private GitHub and GitLab providers, which resolve files from the release assets, ignore blockMapUrl). With every provider, including custom ones, a URL that electron-updater resolves against the feed URL (such as files[].url or a blockmap) gets the feed query only on the feed's origin. Serve the update files from the feed origin (relative files[].url, the default) or use pre-signed URLs. Signed update manifests cover files[].url and packages.<arch>.path, so a pre-signed URL has to be in latest*.yml before it is signed (see Key storage). A custom provider (one that does not extend a built-in provider such as GenericProvider) declares where these headers go by overriding Provider.feedBaseUrl: return its feed URL to send them only to that origin, or null to send the request headers to every download URL. If it declares neither and the downloads carry credential headers, downloadUpdate() fails with ERR_UPDATER_FEED_BASE_URL_NOT_DECLARED before any download request:
class MyProvider extends Provider<UpdateInfo> {
get feedBaseUrl(): URL | null {
return new URL("https://updates.example.com/") // or null
}
// getLatestVersion(), resolveFiles(), …
}:::
Custom downloaded-file verification
All updater classes inherit AppUpdater.verifyUpdateFile, which lets you run your own verification step on an update file before it is allowed to become installable.
If the verification fails, the update is aborted, the file is deleted and an ERR_UPDATER_INVALID_UPDATE_FILE error is emitted.
The hook runs on every path that can lead to an install, so a rejected file can never reach the installer:
| When | updateFilePath points to |
|---|---|
| Right after a fresh download | the file under a temporary name — it is renamed to originalUpdateFileName only after your verification succeeds, so an unverified file is never executable under its real name |
| An update downloaded by an earlier session and reused from the cache | the cached file, under its real name |
| Before an install-on-next-launch spawns the cached installer | the pending installer, under its real name |
Only the first case can quarantine the file under a temporary name; in the other two the bytes are already on disk under their real name from a previous session, and the hook is a re-verification of a file at rest. cancellationToken is set only when the verification belongs to a download.
For NSIS web installers, packageFilePath is also provided and points to the downloaded companion .7z package. It is omitted when no such package was downloaded.
This hook is generic and works across updater implementations. On Windows NSIS updates, the built-in Authenticode verification remains available separately as NsisUpdater.verifyUpdateFileAuthenticodeSignature, and both run — Authenticode first, then your hook.
import { NsisUpdater } from "electron-updater"
const updater = new NsisUpdater()
updater.verifyUpdateFile = async ({ updateFilePath, originalUpdateFileName, packageFilePath, cancellationToken }) => {
try {
// Example for your custom code, which can inspect the update file at `updateFilePath`,
// can use the expected filename `originalUpdateFileName`, and for NSIS web installers can also inspect
// the downloaded companion package at `packageFilePath`.
const signatureFile = getCorrespondingSignatureFile(originalUpdateFileName)
checkSignature(updateFilePath, signatureFile)
return { response: "success" }
}
catch (err) {
return { response: "failure", message: `${err}` }
}
}
// assigning null restores the default (a stub that always succeeds)
updater.verifyUpdateFile = nullA failure must carry a message; it is what the emitted error reports as the reason.
Install on Next Launch (Windows/Linux)
When a downloaded update is automatically installed is controlled by autoUpdater.autoInstallEvent ("manual" | "onQuit" | "onNextLaunch", default "onQuit"). With the default "onQuit", the update is installed when the app quits: the updater spawns the installer as a detached process while the app is exiting. If the quit happens because the OS session is ending (shutdown, reboot or log off on Windows), the OS can kill that installer mid-install and leave the app in a broken, partially-uninstalled state (#7807).
electron-updater ≥ 7.0 (electron-builder v27) mitigates this in two ways:
Session-end guard (always on). When the OS signals that the session is ending, the on-quit install is skipped and a warning is logged. The downloaded update stays cached and is installed on the next regular quit. Detection is best-effort:
powerMonitorshutdownon macOS/Linux, and theBrowserWindowsession-endevent on Windows (windowless, e.g. tray-only, apps cannot be covered on Windows).Install on next launch (opt-in). Instead of installing while quitting, persist the downloaded update and install it at the start of the next launch, when no session teardown can interrupt it:
jsautoUpdater.autoInstallEvent = "onNextLaunch"With this set, any app quit records the downloaded update as pending instead of spawning the installer. On the next launch the updater fetches fresh update info from your update server, re-validates the cached installer against it (checksum, and code signature on Windows), verifies the pending version is still an installable change (newer than the running app — or a downgrade when
allowDowngradeis enabled), then runs the installer silently and restarts the app. If validation fails or the version is no longer an installable change, the pending state is cleared and the app starts normally.Set
autoInstallEvent = "manual"to disable automatic installation entirely (the downloaded update stays cached until you callquitAndInstall()yourself).A single quit can also be deferred without setting the property:
jsautoUpdater.quitAndInstall({ isSilent: true, isForceRunAfter: true, waitUntilNextLaunch: true })You can also trigger the pending install explicitly (e.g. before opening your first window):
jsawait autoUpdater.installPendingUpdateIfAvailable()
Per-target behavior
The automatic install at startup only runs for targets that can install the pending update without an elevation/authentication prompt. Targets that always elevate to install (deb/rpm/pacman via pkexec/sudo, per-machine NSIS via UAC) skip the automatic path with a log message and keep the pending update — call installPendingUpdateIfAvailable() explicitly at a moment your app controls to install those.
| Target | Automatic install at next launch | Explicit installPendingUpdateIfAvailable() |
|---|---|---|
| NSIS (per-user) | ✓ | ✓ |
NSIS (per-machine, isAdminRightsRequired) |
skipped — would show a UAC prompt at startup | ✓ (UAC prompt) |
| AppImage | ✓ | ✓ |
| deb / rpm / pacman | skipped — package managers always elevate (pkexec/sudo) | ✓ (auth prompt) |
| macOS | n/a — Squirrel.Mac stages updates natively and applies them on relaunch | resolves false |
electron-builder v27 sets isAdminRightsRequired in the update info of every per-machine nsis and nsis-web build (perMachine: true), including assisted installers (oneClick: false) and builds with differentialPackage: false, so their updates are started with elevate.exe and skipped by the automatic install at launch.
:::note[Planned default change in v28]autoInstallEvent defaults to "onQuit" in v27; "onNextLaunch" is planned to become the default in v28 to resolve this class of session-end corruption once and for all. macOS is unaffected: Squirrel.Mac natively stages downloaded updates and applies them on relaunch, without a killable installer process (there "onQuit" and "onNextLaunch" behave identically).
:::
:::caution[Launching during an OS shutdown is a residual, best-effort-only race]
Deferring the install to the next launch moves the installer out of the session-end quit — the common #7807 trigger — but it does not make the install itself atomic. If the user launches the app while the OS is already shutting down or rebooting (for example, right before a scheduled restart fires), the automatic startup install spawns the same detached NSIS/AppImage installer, and the OS can still terminate it mid-write.
The session-end guard does not cover this launch window: it is wired into the on-quit path only, and on a fresh launch the shutdown / session-end signal generally arrives after the startup install has already been spawned, so it cannot reliably win the race. The probability is very low — it requires a deferred update, a launch inside the shutdown window, and the OS killing the installer mid-write — and macOS is immune (Squirrel.Mac swaps the app bundle atomically). The only complete fix for NSIS/AppImage is an atomic install, which is out of scope for this feature.
:::
Debugging
You don't need to listen to all events to understand what's wrong. Just set logger.
electron-log is recommended (it is an additional dependency that you can install if needed).
autoUpdater.logger = require("electron-log")
autoUpdater.logger.transports.file.level = "info"Note that in order to develop/test UI/UX of updating without packaging the application you need to have a file named dev-app-update.yml in the root of your project, which matches your publish setting from electron-builder config (but in yaml format).
In latest version you need force the updater to work in "dev" mode:
autoUpdater.forceDevUpdateConfig = true:::note
If you see this in logs:
APPIMAGE env is not defined, current application is not an AppImageyou need to apply this workaround otherwise update won't continue:
process.env.APPIMAGE = path.join(__dirname, 'dist', `app_name-${app.getVersion()}.AppImage`):::
But it is not recommended, better to test auto-update for installed application (especially on Windows). Minio is recommended as a local server for testing updates.
Compatibility
Since electron-builder 27 the generated latest*.yml targets the modern files[] metadata format by default and no longer includes the legacy top-level path/sha512 fields, which are needed only by electron-updater 1.x – 2.15.0.
The electronUpdaterCompatibility option sets the electron-updater compatibility semver range (e.g. >=2.16, >=1.0.0). Can be specified per platform (e.g. win.electronUpdaterCompatibility). When the declared range intersects legacy electron-updater versions, the corresponding legacy fields are emitted in addition to files[]: the top-level path/sha512 and the Windows sha2 checksum for ranges intersecting <2.16.0, and the old latest-mac.json for ranges intersecting <2.0.0. Defaults to >=2.16.
Metadata format history:
1.0.0latest-mac.json2.15.0path2.16.0files
:::warning[sha2-only metadata is deprecated]
Update metadata validated only by the legacy SHA-256 sha2 checksum is deprecated — v27 warns and v28 rejects it (fail-closed). Avoid pinning electronUpdaterCompatibility to a legacy range unless you actually ship 1.x–2.15 clients.
:::
Update security options
Two AppUpdater settings changed or were added in v27. See the Security & Hardening page for the full rationale.
The update manifest (latest*.yml) itself is cryptographically signed and verified before any download as of v27. Publishing a build that emits update metadata fails unless an Ed25519 signing key resolves; updateManifest: false is the only opt-out — see Signed Update Manifests.
Changing a signing key or certificate that installs in the field already trust requires a transition release — see Key Rotation.
`disableWebInstaller` (now defaults to `true`)
NSIS web installers download their full payload at install time from a manifest-supplied URL, which may not undergo signature verification. As of v27, AppUpdater.disableWebInstaller defaults to true, so a web-installer update is not loaded unless you opt in.
Unless the flag is false, a web-installer update is rejected with ERR_UPDATER_WEB_INSTALLER_DISABLED — at download time (including an update already cached by a previous launch), before an install on next launch, and at install time.
Installs made by an nsis-web installer built with electron-builder v27+ carry a resources/package-type marker, and NsisUpdater then defaults the flag to false automatically. Set it yourself only for web-installer installs without that marker (installed by an installer built before v27, or with a custom script) or when switching an app from nsis to nsis-web:
import { NsisUpdater } from "electron-updater"
const updater = new NsisUpdater()
updater.disableWebInstaller = false // only if you intentionally ship a web installerSet it before the app is ready, e.g. right after creating the updater: with autoInstallEvent: "onNextLaunch", a pending web-installer update is checked against the flag when the app is ready, and rejected (its pending-install marker cleared) while the flag is true.
A web update cached by a previous launch, or pending an install on next launch, is used only if its web package still matches the freshly fetched manifest; otherwise it is discarded. The nsis-web installer itself verifies the package electron-updater passes to it and installs its own copy — see Web Installer. Once the app runs the version of that update, NsisUpdater removes the package from its pending cache directory at startup (app ready); the package of an update that is not installed yet, or whose install failed, is kept for the install or a retry. Because the manifest vouches for the web package, don't set updateManifest: false for nsis-web apps: signed update manifests cover its path, SHA-512 and size.
`allowUnverifiedLinuxPackages` (new)
AppUpdater.allowUnverifiedLinuxPackages (default true, preserving historical behavior) controls GPG signature enforcement when installing .deb / .rpm auto-updates. Because electron-builder does not sign Linux packages itself, the default is permissive; set it to false to enforce signature checks where the package manager supports them:
import { autoUpdater } from "electron-updater"
autoUpdater.allowUnverifiedLinuxPackages = falseStaged Rollouts
Staged rollouts allow you to distribute the latest version of your app to a subset of users that you can increase over time, similar to rollouts on platforms like Google Play.
Staged rollouts are controlled by manually editing your latest.yml / latest-mac.yml (channel update info file).
version: 1.1.0
files:
- url: TestApp Setup 1.1.0.exe
sha512: Dj51I0q8aPQ3ioaz9LMqGYujAYRbDNblAQbodDRXAMxmY6hsHqEl3F6SvhfJj5oPhcqdX1ldsgEvfMNXGUXBIw==
size: 62021782
stagingPercentage: 10Update will be shipped to 10% of userbase.
If you want to pull a staged release because it hasn't gone well, you must increment the version number higher than your broken release.
Because some of your users will be on the broken 1.0.1, releasing a new 1.0.1 would result in them staying on a broken version.
File Generated and Uploaded in Addition
latest.yml (or latest-mac.yml for macOS, or latest-linux.yml for Linux) will be generated and uploaded for all providers except bintray (because not required, bintray doesn't use latest.yml).
Private GitHub Update Repo
You can use a private repository for updates with electron-updater by setting the GH_TOKEN environment variable (on user machine) and private option.
If GH_TOKEN is set, electron-updater will use the GitHub API for updates allowing private repositories to work.
:::warning
Private GitHub provider only for very special cases — not intended and not suitable for all users.
:::
:::note
The GitHub API currently has a rate limit of 5000 requests per user per hour. An update check uses up to 3 requests per check.
:::
Events
The autoUpdater object emits the following events:
Event: `error`
errorError
Emitted when there is an error while updating.
Event: `checking-for-update`
Emitted when checking if an update has started.
Event: `update-available`
infoUpdateInfo (for generic and github providers)
Emitted when there is an available update. The update is downloaded automatically ifautoDownloadistrue.
Event: `update-not-available`
Emitted when there is no available update.
infoUpdateInfo (for generic and github providers)
Event: `download-progress`
progressProgressInfobytesPerSecondpercenttotaltransferred
Emitted on progress.
Event: `update-downloaded`
infoUpdateInfo — for generic and github providers.
Event: `update-cancelled`
infoUpdateInfo
Emitted when an update is cancelled by the user or the download is otherwise aborted.
Event: `appimage-filename-updated`
pathstring
Emitted after a successful AppImage update when the file is moved to its final location.
Only fires on Linux AppImage builds.
UpdateInfo
{!./electron-updater.Interface.UpdateInfo.md!}
Features Build Lifecycle (`website/docs/features/build-lifecycle.md`)
Build Lifecycle
Every electron-builder build run passes through a fixed sequence of phases: dependency resolution, app staging, code signing, distributable assembly, and artifact publication. Understanding this sequence tells you which hook to use, when it fires, and what state the filesystem is in when your code runs.
Complete Build Flow
electron-builder build
│
├─ 1. Configuration & Validation
│ └─ Load config, merge platform defaults, resolve output paths
│
├─ 2. For each Platform × Architecture
│ │
│ ├─ 2a. Install / Rebuild Native Deps
│ │ └─ 🪝 beforeBuild
│ │
│ ├─ 2b. Pre-Pack
│ │ └─ 🪝 beforePack
│ │
│ ├─ 2c. Extract Electron Binary → staging dir
│ │ └─ 🪝 afterExtract
│ │
│ ├─ 2d. Copy App Files
│ │ ├─ Filter via files / file patterns
│ │ ├─ Pack node_modules (honor onNodeModuleFile)
│ │ ├─ Create ASAR archive (if enabled)
│ │ ├─ Copy extraResources
│ │ └─ Copy extraFiles
│ │
│ ├─ 2e. Post-Pack (before signing)
│ │ └─ 🪝 afterPack
│ │
│ ├─ 2f. Apply Electron Fuses (if configured)
│ │
│ ├─ 2g. Code Sign the .app / .exe
│ │ └─ 🪝 afterSign (only fires if signing actually ran)
│ │
│ └─ 2h. Build Distributables — For each Target
│ ├─ 🪝 artifactBuildStarted
│ ├─ [Target-specific build — see per-target section below]
│ └─ 🪝 artifactBuildCompleted
│ └─ Publish Manager schedules artifact upload
│
├─ 3. All Builds Complete
│ └─ 🪝 afterAllArtifactBuild
│
└─ 4. Publish
└─ Upload all queued artifacts to configured providersPhase-by-Phase Reference
Phase 1 — Configuration & Validation
electron-builder reads and merges configuration from (in priority order): CLI flags, electron-builder.config.*, package.json#build. Platform-specific options (mac, win, linux) are overlaid on top of the base config. Output directories are resolved and created.
No hooks fire here.
Phase 2a — Install / Rebuild Native Dependencies
Before staging the app, electron-builder optionally rebuilds native Node.js add-ons (node-gyp rebuild) for the target platform and architecture. This happens once per platform/arch combination.
🪝 `beforeBuild`
| Property | Value |
|---|---|
| Fires | Before native dependency install/rebuild |
Return false |
Skips native dependency installation entirely |
| Context type | BeforeBuildContext |
beforeBuild: async ({ appDir, electronVersion, platform, arch }) => {
console.log(`Building native deps for ${platform.name} ${arch}`)
// return false to skip; useful when node_modules are managed externally
}See also: Loading App Dependencies Manually
Phase 2b — Pre-Pack
Fires before any file copying begins. The staging directory (appOutDir) has been created but is empty.
🪝 `beforePack`
| Property | Value |
|---|---|
| Fires | Before files are copied into the app bundle |
| Staging dir state | Empty |
| Context type | BeforePackContext |
beforePack: async ({ outDir, appOutDir, packager, electronPlatformName, arch, targets }) => {
// Generate files that need to be in the build
}Phase 2c — Extract Electron Binary
The Electron binary for the target platform/arch is downloaded (or read from cache) and extracted into the staging directory. After this phase the directory contains the unmodified Electron shell — no app code yet.
🪝 `afterExtract`
| Property | Value |
|---|---|
| Fires | After Electron is extracted; before any app files are placed |
| Staging dir state | Contains Electron binary only |
| Context type | AfterExtractContext (same shape as PackContext) |
afterExtract: async ({ outDir, appOutDir, packager, electronPlatformName, arch, targets }) => {
// Modify the Electron binary itself, e.g. replace embedded resources
}To provide a fully custom Electron build instead of downloading the standard release, use the electronDist hook:
🪝 `electronDist`
| Property | Value |
|---|---|
| Fires | Before Electron extraction, to override the source |
| Return | Path to a custom Electron directory or a folder of zip files |
electronDist: async (context) => {
return "/path/to/custom-electron-dist"
}Phase 2d — Copy App Files
This is the main packaging phase. electron-builder:
- Computes the file set based on the
filesglob patterns (defaulting to everything exceptnode_modulesand dev artifacts). - Filters
node_modules— only production deps are included by default. - Creates an ASAR archive (enabled by default). In v27 the
asar: truesentinel was removed — omit the key to keep defaults, pass anasar: {}object to configure it, orasar: falseto disable. - Copies
extraResourcesinto the platform resources directory. - Copies
extraFilesinto the app root.
🪝 `onNodeModuleFile`
| Property | Value |
|---|---|
| Fires | Once per file inside node_modules during packaging |
Return true |
Force-include the file |
Return false |
Force-exclude the file |
Return undefined/void |
Use default inclusion logic |
onNodeModuleFile: (filePath) => {
if (filePath.includes("__tests__") || filePath.endsWith(".test.js")) {
return false // exclude test files from all modules
}
}No other hooks fire during file copying. Use afterPack (next phase) to inspect or modify the result.
Phase 2e — Post-Pack (before signing)
All app files are in the staging directory. The app bundle is complete but unsigned. This is the primary hook for modifying the packaged app.
🪝 `afterPack`
| Property | Value |
|---|---|
| Fires | After all files are packaged; before code signing |
| Staging dir state | Complete app bundle, unsigned |
| Context type | AfterPackContext |
afterPack: async ({ outDir, appOutDir, packager, electronPlatformName, arch, targets }) => {
// appOutDir points to the staged .app / win-unpacked / linux-unpacked dir
const { join } = require("path")
const { writeFileSync } = require("fs")
writeFileSync(join(appOutDir, "VERSION"), packager.appInfo.version)
}Common uses:
- Inject a
VERSIONorBUILD_IDfile into the bundle - Modify
Info.plist(macOS) orresources/app.asarcontents - Strip debug symbols / strip binaries before signing
Phase 2f — Apply Electron Fuses
If electronFuses is configured, fuse bits are flipped in the Electron binary at this point — after packing, before signing. Fuses are baked into the binary and cannot be changed post-sign.
Phase 2g — Code Sign
The app bundle is signed using the platform-native toolchain:
| Platform | Tool | What is signed |
|---|---|---|
| macOS | codesign |
.app bundle, all frameworks and dylibs |
| Windows | signtool.exe |
EXE and DLL files |
| Linux | (no signing in this phase) | — |
Notarization (macOS) runs here when mac.notarize: true is set.
🪝 `afterSign`
| Property | Value |
|---|---|
| Fires | After signing completes; only if signing actually ran |
| Not fired | When signing is skipped (no certificate configured) |
| Context type | AfterPackContext |
afterSign: async ({ outDir, appOutDir, packager, electronPlatformName, arch, targets }) => {
// App is signed. Distributables have not been built yet.
}:::tip[Built-in notarization]
Use mac.notarize: true for standard notarization — electron-builder handles it automatically. Only reach for afterSign when you need a custom notarization flow. See Notarization.
:::
For a fully custom signing implementation (replacing the built-in signer):
# electron-builder.config.yml
mac:
sign: ./scripts/custom-sign.js
win:
sign:
type: signtool
sign: ./scripts/custom-sign.js
publisherName: "CN=My Company, O=My Company, C=US"On Windows, electron-builder cannot read the certificate a custom hook signs with, so set win.sign.publisherName to that certificate's subject — it is written to app-update.yml for update signature verification, and a build that writes app-update.yml fails without it. Copy it from a binary the hook already signed, as shown in How do you delegate code signing?; every component you list must match exactly.
Phase 2h — Build Distributables (per target)
For each requested output format (NSIS, DMG, AppImage, etc.), electron-builder assembles the final distributable from the staged app. Target builds run after signing, so the inputs are always signed binaries.
Two hooks bracket every target build:
🪝 `artifactBuildStarted`
| Property | Value |
|---|---|
| Fires | Immediately before a single artifact starts building |
| Context type | ArtifactBuildStarted |
artifactBuildStarted: async ({ targetPresentableName, file, safeArtifactName, packager, arch }) => {
console.log(`Starting: ${targetPresentableName}`)
}🪝 `artifactBuildCompleted`
| Property | Value |
|---|---|
| Fires | Immediately after a single artifact finishes building |
| Context type | ArtifactCreated |
artifactBuildCompleted: async ({ file, safeArtifactName, target, packager, arch, sha512 }) => {
console.log(`Done: ${file} SHA512: ${sha512}`)
}After artifactBuildCompleted fires, the Publish Manager queues the artifact for upload and schedules update-metadata generation (latest.yml, latest-mac.yml, etc.).
Target-specific hooks
Some targets expose additional hooks that fire inside their own build sequence:
| Hook | Target | Fires when |
|---|---|---|
msiProjectCreated |
MSI (WiX) | After WiX .wxs file is written to disk, before candle.exe / light.exe |
appxManifestCreated |
AppX / MSIX | After AppxManifest.xml is written to disk, before makeappx.exe |
// Edit the WiX XML before MSI compilation
msiProjectCreated: async (wixProjectPath) => {
const fs = require("fs")
const wxsPath = require("path").join(wixProjectPath, "installer.wxs")
let xml = fs.readFileSync(wxsPath, "utf8")
xml = xml.replace('Manufacturer="PLACEHOLDER"', 'Manufacturer="Acme Corp"')
fs.writeFileSync(wxsPath, xml)
}// Edit the AppX manifest before packaging
appxManifestCreated: async (manifestPath) => {
const fs = require("fs")
let manifest = fs.readFileSync(manifestPath, "utf8")
manifest = manifest.replace(/<DisplayName>.*?<\/DisplayName>/, "<DisplayName>My App</DisplayName>")
fs.writeFileSync(manifestPath, manifest)
}Phase 3 — All Builds Complete
Every platform, architecture, and target has finished. All artifacts are on disk.
🪝 `afterAllArtifactBuild`
| Property | Value |
|---|---|
| Fires | After every platform/arch/target finishes; before publishing |
| Context type | BuildResult |
| Return | string[] — additional file paths to include in publish |
afterAllArtifactBuild: async ({ outDir, artifactPaths, platformToTargets, configuration }) => {
// Upload debug symbols
const dsymPaths = artifactPaths.filter(p => p.endsWith(".dSYM"))
for (const p of dsymPaths) {
await uploadToSentry(p)
}
// Return extra files to include in the publish step
const changelog = `${outDir}/CHANGELOG.md`
writeChangelog(changelog)
return [changelog]
}Phase 4 — Publish
The Publish Manager uploads all queued artifacts (and any extra paths returned from afterAllArtifactBuild) to the configured providers: GitHub Releases, S3, DigitalOcean Spaces, Generic server, GitLab, Keygen, Snap Store, etc.
Update metadata files (latest.yml, latest-mac.yml, latest-linux.yml) are written to the output directory and included in the publish.
See Publish Configuration for provider setup.
Hook Summary Table
| Hook | Phase | What the filesystem looks like | Common use |
|---|---|---|---|
beforeBuild |
2a | Source tree only | Skip or customize native rebuild |
beforePack |
2b | Empty staging dir | Inject generated source files |
afterExtract |
2c | Staging: Electron binary only | Modify the Electron binary |
electronDist |
2c | (before extraction) | Supply a custom Electron build |
onNodeModuleFile |
2d | (during file copy) | Filter node_modules inclusions |
afterPack |
2e | Staging: full app bundle, unsigned | Modify bundle before signing |
afterSign |
2g | Staging: full app bundle, signed | Custom post-sign steps |
artifactBuildStarted |
2h | Target build starting | Logging, timing |
msiProjectCreated |
2h (MSI) | WiX .wxs file on disk |
Edit WiX XML |
appxManifestCreated |
2h (AppX) | AppxManifest.xml on disk |
Edit AppX manifest |
artifactBuildCompleted |
2h | Artifact file on disk | Checksums, per-artifact upload |
afterAllArtifactBuild |
3 | All artifacts on disk | Symbol upload, extra publish paths |
Target Build Sequences
Each target runs inside Phase 2h. Here is what happens inside each one:
Windows
| Target | Sequence |
|---|---|
| NSIS | Generate script → compile with makensis → sign installer → emit artifact |
| MSI (WiX) | Write .wxs → msiProjectCreated hook → candle.exe + light.exe → sign → emit |
| AppX / MSIX | Write manifest → appxManifestCreated hook → makeappx.exe + makepri.exe → sign → emit |
| Portable | Copy staged app into a self-extracting archive → emit |
macOS
| Target | Sequence |
|---|---|
| DMG | Create .dmg via dmgbuild → emit |
| ZIP | Archive .app bundle → emit |
| PKG | Build .pkg via pkgbuild + productbuild → sign → emit |
| MAS | Re-sign with MAS provisioning profile → build pkg → emit |
Linux
| Target | Sequence |
|---|---|
| AppImage | Create squashfs image → embed ELF header → emit |
| DEB / RPM / Pacman | Package via fpm → emit |
| Snap | Write snapcraft.yaml → run snapcraft → emit |
| Flatpak | Write manifest → run flatpak-builder → emit |
Concurrency
By default electron-builder packs one platform/architecture combination at a time (concurrency.jobs: 1). Raising this value runs multiple platform × arch pack operations in parallel using an async pool.
# electron-builder.config.yml
concurrency:
jobs: 2 # pack up to 2 platform/arch combos simultaneouslyWhat concurrency affects
| Scope | Behavior |
|---|---|
| Platform × arch loop | Up to jobs pack operations run in parallel |
| Hooks within one pack | Always serial — each hook is fully awaited before the build advances |
afterAllArtifactBuild |
Fires once, after all concurrent packs complete |
So with jobs: 2 and a build targeting mac/x64, mac/arm64, and win/x64:
mac/x64andmac/arm64start concurrently (2 slots)win/x64starts as soon as one of the mac slots finishesafterAllArtifactBuildfires after the last one completes
Concurrency limit
Setting jobs above 8 (the internal MAX_FILE_REQUESTS constant) logs a warning because each concurrent pack opens many file handles simultaneously. Exceeding the OS file descriptor limit causes EMFILE errors. The value is floored to an integer; any value less than 1 is reset to 1.
concurrency:
jobs: 4 # safe upper bound on most systemsHook author implications
Because hooks for different platform/arch combinations may run at the same time, avoid writing to shared paths without coordination. Each hook receives its own appOutDir, outDir, and arch so there is no conflict when writing into the staging directory — the risk is only if your hook writes to a fixed global path (a log file, a shared temp directory, etc.).
See Multi Platform Build and Build Architectures.
Further Reading
- Hooks Reference — full API for every hook, with examples
- File Patterns — how
files,extraResources, andextraFileswork - Code Signing — signing setup for macOS and Windows
- Publish Configuration — provider setup and update metadata
- Programmatic Usage — drive the build from Node.js
Features Electron Forge (`website/docs/features/electron-forge.md`)
title: "Electron Forge Integration"
electron-builder ships four Electron Forge maker packages that let you produce electron-builder targets from a Forge project without migrating your entire build pipeline.
:::tip[Consider using electron-builder directly]
Publishing, Auto Update, and Code Signing are only available when using electron-builder as your primary build tool. If you need any of those features, migrate fully to electron-builder rather than using these makers.
:::
How it works
Each maker is a thin wrapper around buildForge() from app-builder-lib. When Electron Forge calls make, the maker delegates to electron-builder's full packaging pipeline for that target, then returns the paths to the produced artifacts. All electron-builder configuration options for the target are available via the maker's config key.
Available makers
| Package | Target | Platform |
|---|---|---|
electron-forge-maker-nsis |
NSIS installer (.exe) |
Windows |
electron-forge-maker-nsis-web |
Web installer (.exe, downloads payload at install time) |
Windows |
electron-forge-maker-appimage |
AppImage (.AppImage) |
Linux |
electron-forge-maker-snap |
Snap package (.snap) |
Linux |
NSIS installer
npm install --save-dev electron-forge-maker-nsisconst { MakerNsis } = require("electron-forge-maker-nsis")
module.exports = {
makers: [
new MakerNsis({
// All NsisOptions are accepted here — see /docs/nsis for the full reference
oneClick: false,
perMachine: true,
}),
],
}Full configuration reference: NSIS
NSIS web installer
npm install --save-dev electron-forge-maker-nsis-webconst { MakerNsisWeb } = require("electron-forge-maker-nsis-web")
module.exports = {
makers: [
new MakerNsisWeb({
// NsisWebOptions — extends NsisOptions
}),
],
}The web installer produces a small stub executable that detects the user's architecture and downloads the appropriate payload from your publish provider at install time. Full configuration reference: NSIS
AppImage
npm install --save-dev electron-forge-maker-appimageconst { MakerAppImage } = require("electron-forge-maker-appimage")
module.exports = {
makers: [
new MakerAppImage({
// AppImageOptions — see /docs/appimage for the full reference
license: "LICENSE",
}),
],
}Full configuration reference: AppImage
Snap
npm install --save-dev electron-forge-maker-snapconst { MakerSnap } = require("electron-forge-maker-snap")
module.exports = {
makers: [
new MakerSnap({
// SnapOptions — see /docs/snap for the full reference
confinement: "strict",
grade: "stable",
}),
],
}Full configuration reference: Snap
Limitations
These makers delegate to electron-builder's packaging only. The following electron-builder features are not available through Forge makers:
- Publishing — use
electron-builder --publishdirectly - Auto Update — requires electron-builder's full publish pipeline; see Auto Update
- Code Signing — requires electron-builder's signing configuration; see Code Signing
For projects that need any of these, the recommended path is to use electron-builder as the primary build tool instead of Electron Forge.
Features Github Actions (`website/docs/features/github-actions.md`)
GitHub Actions CI/CD
This guide shows how to build, sign, and publish Electron apps using GitHub Actions for all three platforms (macOS, Windows, Linux).
Basic Concepts
- Secrets — store certificates, passwords, and API tokens in GitHub repository secrets (Settings → Secrets and variables → Actions)
- Matrix builds — build for multiple platforms in parallel using a matrix strategy
GH_TOKEN— the GitHub token electron-builder uses to upload release assets. UseGITHUB_TOKEN(provided automatically) or a PAT withreposcope
Minimal Multi-Platform Workflow
This workflow builds for macOS, Windows, and Linux on every push to main and on version tags:
name: Build and Release
on:
push:
branches: [main]
tags: ['v*']
jobs:
build:
strategy:
matrix:
os: [macos-latest, windows-latest, ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Build
run: npx electron-builder --publish never
env:
# macOS signing
CSC_LINK: ${{ secrets.MAC_CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.MAC_CSC_KEY_PASSWORD }}
# Windows signing
WIN_CSC_LINK: ${{ secrets.WIN_CSC_LINK }}
WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: dist-${{ matrix.os }}
path: dist/Publish on Tag Push
To publish GitHub releases automatically when you push a version tag (v1.2.3):
name: Release
on:
push:
tags: ['v*']
jobs:
release:
strategy:
matrix:
os: [macos-latest, windows-latest, ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Publish
run: npx electron-builder --publish always
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# macOS signing + notarization
CSC_LINK: ${{ secrets.MAC_CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.MAC_CSC_KEY_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
# Windows signing
WIN_CSC_LINK: ${{ secrets.WIN_CSC_LINK }}
WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}The --publish always flag uploads artifacts to the GitHub release for the pushed tag. If no release exists, electron-builder creates a draft release.
Draft Release Workflow
A common pattern is to publish a draft release on every tagged push, then manually promote it to a full release after reviewing:
# In electron-builder.yml
publish:
provider: github
releaseType: draftThen in GitHub: go to Releases → find the draft → click "Edit" → "Publish release".
macOS Code Signing Setup
Preparing the Certificate
# Export your Developer ID Application certificate from Keychain Access
# File → Export Items → save as .p12 with a password
# Encode to base64
base64 -i certificate.p12 | pbcopy # copies to clipboardAdd to GitHub Secrets:
MAC_CSC_LINK— the base64-encoded.p12contentMAC_CSC_KEY_PASSWORD— the certificate password
macOS Signing
Pass CSC_LINK and CSC_KEY_PASSWORD directly — electron-builder creates and manages a temporary keychain automatically:
build-mac:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Build and notarize
run: npx electron-builder --mac --publish always
env:
CSC_LINK: ${{ secrets.MAC_CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.MAC_CSC_KEY_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}macOS Secrets Reference
| Secret | How to Get |
|---|---|
MAC_CSC_LINK |
base64 -i certificate.p12 |
MAC_CSC_KEY_PASSWORD |
The password you set when exporting the .p12 |
APPLE_ID |
Your Apple ID email (developer account) |
APPLE_APP_SPECIFIC_PASSWORD |
Create at appleid.apple.com → App-Specific Passwords |
APPLE_TEAM_ID |
10-character team ID from developer.apple.com/account |
Windows Code Signing Setup
# Encode your Windows certificate (.pfx) to base64
# PowerShell:
[Convert]::ToBase64String([IO.File]::ReadAllBytes("certificate.pfx")) | Out-File cert_encoded.txt
# bash (Linux/macOS):
base64 certificate.pfx > cert_encoded.txtAdd to GitHub Secrets:
WIN_CSC_LINK— base64-encoded.pfxcontentWIN_CSC_KEY_PASSWORD— certificate password
Using Azure Trusted Signing
- name: Build and sign (Azure)
run: npx electron-builder --win --publish always
env:
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}Configure win.sign with type: "azure" in your electron-builder config (this replaces the v26 win.azureSignOptions key). endpoint, codeSigningAccountName, certificateProfileName, and publisherName are required. See Windows Code Signing.
Caching for Faster Builds
Cache npm packages and Electron binaries to avoid redundant downloads:
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm # caches ~/.npm
- name: Cache Electron
uses: actions/cache@v4
with:
path: |
~/.cache/electron
~/.cache/electron-builder
key: ${{ runner.os }}-electron-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-electron-Linux Snapcraft Publishing
To publish to the Snap Store, add a SNAPCRAFT_STORE_CREDENTIALS secret (obtained by running snapcraft export-login -):
- name: Build and publish Snap
run: npx electron-builder --linux snap --publish always
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}Complete Production Workflow
A production-grade workflow with separate jobs per platform, caching, and GitHub release publishing:
/* Detailed source-code truncated for AI context efficiency. */Troubleshooting
"Resource not accessible by integration"
: The workflow doesn't have permission to create releases. Add permissions: contents: write to the job or workflow. Also ensure GH_TOKEN is set.
macOS: "No identity found"
: CSC_LINK or CSC_KEY_PASSWORD is wrong or missing. Verify the base64-encoded certificate decodes to a valid .p12 and that the password is correct. Run echo "$CSC_LINK" | base64 --decode | openssl pkcs12 -info -passin pass:"$CSC_KEY_PASSWORD" locally to verify.
Windows: certificate base64 too long
: Windows CI environments may truncate environment variables over 8192 characters. Re-export the .pfx without the full certificate chain included. See Code Signing.
Build fails with "Cannot find module"
: Native modules need to be rebuilt for the target Electron version. Ensure npm ci (not npm install) is used. electron-builder handles native module rebuilding automatically.
Related Pages
- Code Signing — environment variables, certificate types
- macOS Notarization — notarization setup and troubleshooting
- Auto Update — configuring electron-updater with GitHub releases
- Multi Platform Build — building for other platforms using Docker
Features Hooks (`website/docs/features/hooks.md`)
Build Hooks
Build hooks let you run custom code at specific points in the electron-builder build lifecycle. Use them to modify the app bundle, perform additional signing, upload symbols, or integrate with external services.
Build Lifecycle
Hooks fire in this order during every build. Pick the hook that runs at the right stage for your task — modifying app files before signing, triggering external tools after all artifacts are ready, etc.
| Hook | When it fires | Common use |
|---|---|---|
beforeBuild |
Before native deps are installed/rebuilt | Skip or customize native module rebuild |
beforePack |
Before files are copied into the app bundle | Inject generated files, modify source before pack |
afterExtract |
After Electron binary is extracted | Modify the Electron binary or its resources |
afterPack |
After files are packaged, before signing | Modify app bundle structure, add unpacked files |
afterSign |
After signing, before distributable is created | Custom post-sign steps, extra notarization workflows |
artifactBuildStarted |
When each installer/image starts building | Per-artifact logging or tracking |
artifactBuildCompleted |
When each installer/image finishes | Checksum capture, per-artifact upload |
afterAllArtifactBuild |
After all artifacts are done | Upload debug symbols, generate changelogs, return extra publish paths |
Hook execution is serial within each platform/arch combination. Async hooks are awaited before the build proceeds.
Target-specific hooks (only fire for their respective target):
electronDist— supply a custom Electron binarymsiProjectCreated— edit WiX XML before MSI compilationappxManifestCreated— edit AppX manifest XML before packagingonNodeModuleFile— filter whichnode_modulesfiles are included in the bundle
Defining Hooks
Hooks can be defined in three ways:
1. Inline Function (JS/TS config only)
// electron-builder.config.js
module.exports = {
afterPack: async (context) => {
console.log("afterPack:", context.appOutDir)
}
}2. Path to File
Use a file path string when using JSON/YAML config, or when you want to keep hooks in separate files:
# electron-builder.yml
afterPack: ./scripts/afterPack.js// scripts/afterPack.js
exports.default = async function(context) {
console.log("afterPack:", context.appOutDir)
}The function must be the default export of the module.
3. Module ID
Reference a node module:
afterPack: my-electron-builder-pluginHook Reference
`beforeBuild`
Runs before native dependencies are installed or rebuilt (before npm rebuild). Resolving to false skips dependency installation entirely.
beforeBuild: async (context) => {
// context: BeforeBuildContext
// { appDir, electronVersion, platform, arch }
console.log("Building for:", context.platform.name, context.arch)
// return false to skip dependency install
}`beforePack`
Runs before files are copied into the app bundle.
beforePack: async (context) => {
// context: BeforePackContext
// { outDir, appOutDir, packager, electronPlatformName, arch, targets }
}`afterExtract`
Runs after the Electron binary has been extracted into the output directory, before app files are placed. Useful for manipulating the Electron binary or its resources.
afterExtract: async (context) => {
// context: AfterExtractContext (same shape as PackContext)
// { outDir, appOutDir, packager, electronPlatformName, arch, targets }
}`afterPack`
Runs after the app is fully packaged but before code signing. This is the ideal hook for:
- Modifying the app bundle structure
- Adding files that should not be ASAR-archived
- Running post-processing on the packaged app
afterPack: async (context) => {
// context: AfterPackContext
// { outDir, appOutDir, packager, electronPlatformName, arch, targets }
const path = require("path")
const fs = require("fs")
// Example: add a VERSION file to the app bundle
const versionFile = path.join(context.appOutDir, "VERSION")
fs.writeFileSync(versionFile, context.packager.appInfo.version)
}`afterSign`
Runs after the app is signed but before the distributable (DMG, NSIS installer, etc.) is created. Commonly used for macOS notarization in custom workflows.
afterSign: async (context) => {
// context: AfterPackContext
// { outDir, appOutDir, packager, electronPlatformName, arch, targets }
}:::tip[Notarization]
electron-builder has built-in notarization support via mac.notarize: true. Only use afterSign for notarization if you need a custom notarization workflow. See Notarization.
:::
`artifactBuildStarted`
Runs when an individual artifact (e.g., a DMG, an NSIS installer) starts being built.
artifactBuildStarted: async (context) => {
// context: ArtifactBuildStarted
// { targetPresentableName, file, safeArtifactName, packager, arch }
console.log("Building artifact:", context.targetPresentableName)
}`artifactBuildCompleted`
Runs when an individual artifact finishes building.
artifactBuildCompleted: async (context) => {
// context: ArtifactCreated
// { file, safeArtifactName, target, packager, publishConfig, isWriteUpdateInfo, updateInfo, sha2, sha512, artifactName, arch }
console.log("Built:", context.file, "SHA512:", context.sha512)
}`afterAllArtifactBuild`
Runs after all artifacts have been built. Can return an array of additional file paths to publish.
afterAllArtifactBuild: async (buildResult) => {
// buildResult: BuildResult
// { outDir, artifactPaths, platformToTargets, configuration }
// Example: upload debug symbols to Sentry
for (const artifactPath of buildResult.artifactPaths) {
if (artifactPath.endsWith('.dSYM') || artifactPath.endsWith('.pdb')) {
await uploadSymbols(artifactPath)
}
}
// Return additional files to publish
return ["/path/to/additional-artifact.zip"]
}`electronDist`
Provide a custom Electron binary instead of the standard downloaded one. The hook receives PrepareApplicationStageDirectoryOptions and should return the path to a custom Electron build or a directory of Electron zip files.
electronDist: async (context) => {
// Return path to custom Electron directory
return "/path/to/custom/electron-dist"
}`msiProjectCreated`
Runs after the WiX project has been created on disk but before it is compiled into an MSI. Receives the path to the WiX project directory.
msiProjectCreated: async (wixProjectPath) => {
// Modify WiX XML files at wixProjectPath
}`appxManifestCreated`
Runs after the AppX manifest has been created but before the package is assembled. Receives the path to the manifest file.
appxManifestCreated: async (manifestPath) => {
// Modify the AppX manifest XML
const fs = require("fs")
let manifest = fs.readFileSync(manifestPath, "utf8")
// ... modify manifest ...
fs.writeFileSync(manifestPath, manifest)
}`onNodeModuleFile`
Called for each file in node_modules during packaging. Return true to force-include, false to force-exclude, or undefined/void to use default behavior.
onNodeModuleFile: async (filePath) => {
// Exclude test files from all node_modules
if (filePath.includes("__tests__") || filePath.endsWith(".test.js")) {
return false
}
}Common Hook Patterns
Modifying Info.plist After Pack
const path = require("path")
const plist = require("plist")
const fs = require("fs")
afterPack: async (context) => {
if (context.electronPlatformName !== "darwin") return
const plistPath = path.join(
context.appOutDir,
`${context.packager.appInfo.productFilename}.app`,
"Contents",
"Info.plist"
)
const plistData = plist.parse(fs.readFileSync(plistPath, "utf8"))
plistData.LSUIElement = true // Hide from dock
fs.writeFileSync(plistPath, plist.build(plistData))
}Custom Code Signing
mac:
sign: "./scripts/sign.js"// scripts/sign.js
exports.default = async function(configuration) {
// configuration: CustomMacSign
// { path, packager, entitlements, ... }
const { execSync } = require("child_process")
execSync(`codesign --deep --sign "${configuration.identity}" "${configuration.path}"`)
}Uploading Symbols to Sentry
afterAllArtifactBuild: async (buildResult) => {
const { execSync } = require("child_process")
const dsymPaths = buildResult.artifactPaths.filter(p => p.endsWith(".dSYM"))
for (const dsym of dsymPaths) {
execSync(`sentry-cli upload-dif ${dsym}`)
}
}Generating a Changelog File
afterAllArtifactBuild: async (buildResult) => {
const fs = require("fs")
const changelogPath = `${buildResult.outDir}/CHANGELOG.txt`
fs.writeFileSync(changelogPath, generateChangelog())
return [changelogPath] // include in publish
}Async and Error Handling
All hooks support async functions. If a hook throws an error, the build fails:
afterPack: async (context) => {
try {
await doSomething()
} catch (err) {
console.error("afterPack failed:", err)
throw err // rethrow to fail the build
}
}Debugging Hooks
Add console.log or console.error inside your hooks — output appears in the electron-builder terminal output.
Enable verbose electron-builder logging to see the full build lifecycle:
DEBUG=electron-builder electron-builder buildInterface Reference
{!./app-builder-lib.Interface.Hooks.md!}
Features Icons And Images (`website/docs/features/icons-and-images.md`)
title: "Icons & Images"
electron-builder handles icon and image assets differently per platform. Give it a single high-quality source — a 1024×1024 PNG or an SVG — and it auto-converts to every format the target platform needs. No manual export pipelines, no platform-specific tooling required.
All icon and image files are optional — if omitted, the default Electron icon is used. Files are resolved from the buildResources directory, which defaults to build/ in your project root.
:::tip[Icon creation tools]
Icon Composer, AppIcon Generator, MakeAppIcon
:::
TL;DR — Recommended build/ layout
build/
├── icon.svg ← master icon (SVG — scales perfectly to all sizes)
├── icon.png ← or use PNG (1024×1024 recommended)
├── icon.icns ← optional: pre-built macOS icon (skips conversion)
├── icon.ico ← optional: pre-built Windows icon (skips conversion)
├── background.png ← macOS DMG background (540×380 px)
├── [email protected] ← Retina DMG background (1080×760 px)
└── icons/ ← optional: manual Linux PNG set
├── 16x16.png
├── 32x32.png
└── 512x512.pngA single icon.svg or icon.png in your build/ directory is enough to cover all three platforms — electron-builder discovers and converts it automatically.
Application Icon
The application icon appears in the Dock, taskbar, Start menu, and application menus. electron-builder searches for your icon in this order:
- The explicit
mac.icon,win.icon, orlinux.iconconfig path icon.<format>(e.g.icon.icnsfor macOS,icon.icofor Windows)icon.pngicon.svg— auto-discovered, no config requiredicon.icns(cross-platform source for ICNS-based conversion)- A directory named
icons/oricon/
Format & size requirements
| Platform | Accepted input formats | Minimum | Recommended |
|---|---|---|---|
| macOS | .svg, .png, .icns, .icon |
512×512 | 1024×1024 (or SVG) |
| Windows | .svg, .png, .ico |
256×256 | 512×512 (or SVG) |
| Linux | .svg, .png, directory of PNGs |
256×256 | SVG or 1024×1024 PNG |
:::tip[SVG is best]
SVG icons are rasterized at 1024px before conversion, giving every generated size a clean starting point. Use SVG when your icon is available in vector form.
:::
SVG Support
SVG is supported as an input format for all three platforms. Place icon.svg in your build/ directory and electron-builder finds it automatically — no mac.icon or win.icon config required.
- macOS / Windows: The SVG is rasterized to 1024×1024 before ICNS or ICO conversion.
- Linux (
setformat): The SVG is passed through directly to the freedesktopscalable/icon hierarchy. Desktop environments render it at any scale — ideal for HiDPI and fractional-scaling displays.
Explicit config also works:
mac:
icon: build/icon.svg
win:
icon: build/icon.svgmacOS Icon Sizes (ICNS)
When converting to .icns, electron-builder generates all standard Apple icon sizes:
| Size | Usage |
|---|---|
| 16×16 | Finder list view |
| 32×32 | Finder column view, Retina 16px |
| 64×64 | Retina 32px |
| 128×128 | Finder preview |
| 256×256 | Standard Retina |
| 512×512 | Large Retina |
| 1024×1024 | Full Retina (macOS 10.7+) |
All sizes are generated from the input regardless of the source dimensions — SVG sources and large PNGs produce the sharpest results at every size.
:::note
If you provide a pre-built .icns, electron-builder uses it as-is — no re-encoding.
:::
macOS `.icon` format (Asset Catalog)
If you set mac.icon to a .icon file (Apple Icon Composer asset), electron-builder compiles it into an Assets.car via actool and wires it via CFBundleIconName. This requires Xcode 26+ and macOS 15+ on the build machine.
For .icns files, the path is referenced via CFBundleIconFile and does not require Xcode.
:::note
If you only provide a .icon file and rely on the default DMG volume icon, consider setting dmg.icon explicitly to an .icns file — the DMG volume icon still uses .icns.
:::
Linux Icon Set
electron-builder generates a full set of PNG icons for Linux, named icon_NxN.png, at these standard sizes:
| Size | Freedesktop target directory |
|---|---|
| 16×16 | hicolor/16x16/apps/ |
| 24×24 | hicolor/24x24/apps/ |
| 32×32 | hicolor/32x32/apps/ |
| 48×48 | hicolor/48x48/apps/ |
| 64×64 | hicolor/64x64/apps/ |
| 128×128 | hicolor/128x128/apps/ |
| 256×256 | hicolor/256x256/apps/ |
| 512×512 | hicolor/512x512/apps/ |
SVG sources are placed in scalable/ instead and are not resized.
To provide a pre-built Linux set, create a directory and name each PNG with its pixel dimensions:
linux:
icon: build/icons/build/icons/
├── 16x16.png
├── 32x32.png
├── 48x48.png
├── 128x128.png
├── 256x256.png
└── 512x512.pngFiles must match the pattern NxN.png or N.png (e.g., 256x256.png or 512.png). Files with incidental digits in their names (e.g., app2.png) are ignored.
macOS DMG Background
The DMG installer window background image. The window dimensions match the image dimensions — getting this right ensures your DMG looks polished.
| Asset | Dimensions | Location |
|---|---|---|
| Standard background | 540×380 px | build/background.png or build/background.tiff |
| Retina background | 1080×760 px | build/[email protected] |
| Volume icon | .icns |
build/icon.icns (or dmg.icon) |
dmg:
background: build/background.png
backgroundColor: "#ffffff" # fallback if no background image
icon: build/dmg-icon.icns # volume badge icon (optional)
iconSize: 80
iconTextSize: 12Supply both background.png and [email protected] for crisp rendering on Retina displays.
Windows NSIS Installer Images
The NSIS installer has two visual modes:
- One-click installer — minimal UI, only uses the app icon
- Assisted installer — a wizard with a sidebar and header banner
Assisted installer assets
| Asset | Dimensions | Format | Location |
|---|---|---|---|
| Installer header banner | 150×57 px | .bmp |
build/installerHeader.bmp |
| Installer sidebar | 164×314 px | .bmp |
build/installerSidebar.bmp |
| Uninstaller sidebar | 164×314 px | .bmp |
build/uninstallerSidebar.bmp |
| Installer icon | .ico |
build/installerIcon.ico |
|
| Uninstaller icon | .ico |
build/uninstallerIcon.ico |
nsis:
installerHeader: build/installerHeader.bmp
installerSidebar: build/installerSidebar.bmp
uninstallerSidebar: build/uninstallerSidebar.bmp
installerIcon: build/installerIcon.ico
uninstallerIcon: build/uninstallerIcon.ico:::note
BMP files must be 24-bit RGB (no alpha). Use any image editor to export in this format.
:::
Windows AppX (Microsoft Store) Tile Images
AppX packages require a set of tile images for the Windows Start screen and Store listing. Place them in build/appx/:
| Asset | Dimensions | Filename |
|---|---|---|
| Store logo | 50×50 px | StoreLogo.png |
| Small tile | 44×44 px | Square44x44Logo.png |
| Medium tile | 150×150 px | Square150x150Logo.png |
| Wide tile | 310×150 px | Wide310x150Logo.png |
All AppX images must be PNG with transparency support. Microsoft recommends providing 1x, 1.25x, 1.5x, 2x, and 4x scale variants using the naming convention Square44x44Logo.scale-200.png (for 2x), but electron-builder only requires the base filenames listed above.
See the AppX configuration page for the full set of AppX options, including custom manifest support.
Icons Toolset
electron-builder bundles a portable icon conversion toolset (icon-tool.js + resvg.wasm) that handles all PNG/SVG → ICNS/ICO/set conversions without native dependencies. The toolset is downloaded automatically on first use and cached in the electron-builder cache directory.
Override with a local bundle (useful for offline builds or development). Supply a custom toolset via toolsets.icons (there is no ELECTRON_BUILDER_ICONS_TOOLSET_PATH environment variable):
{ "build": { "toolsets": { "icons": { "url": "file:///path/to/icons-bundle" } } } }The directory must contain icon-tool.js and resvg.wasm (the same layout as the built-in bundle).
Source Resolution Reference
When you specify icon: build/icon (no extension), electron-builder probes the following paths for each target format:
| Priority | Probe path | Notes |
|---|---|---|
| 1 | build/icon.<format> |
e.g. icon.icns for macOS |
| 2 | build/icon |
exact path |
| 3 | build/icon.png |
|
| 4 | build/icon.svg |
auto-discovered; converted on the fly |
| 5 | build/icon.icns |
used as PNG source for non-ICNS targets |
| 6 | build/icons/ or build/icon/ |
directory of pre-sized PNGs |
| 7 | build/icon.ico |
used as source for ICO targets |
ICNS files can be used as a source for all platforms — electron-builder extracts the largest embedded PNG frame and converts from there.
Features Key Rotation (`website/docs/features/key-rotation.md`)
title: "Key Rotation"
Key Rotation
This page is a runbook for rotating the keys that an Electron app's release pipeline depends on: the Ed25519 key that signs update manifests and the platform code-signing certificates for Windows, macOS, and Linux. It is written for maintainers who already ship auto-updating apps with electron-builder and electron-updater.
Why rotation is a transition problem
Every install in the field trusts exactly what it was built with. electron-updater does not fetch trust anchors from your update server, and there is no revocation list it consults. Concretely:
| Trust anchor | Where it is fixed | What checks it |
|---|---|---|
Update-manifest trust list (updateManifestPublicKey, one or more Ed25519 public keys) |
app-update.yml, written at build time — present whenever a signing key or an explicit updateManifest.publicKey resolves (a publishing build fails without a key unless updateManifest: false) |
electron-updater, on every platform, before any download (Signed Update Manifests) |
Windows publisher name (publisherName) |
app-update.yml, written at build time |
electron-updater (NSIS target), against the Authenticode signature of the downloaded installer |
| macOS Team ID | The running app's code signature | Squirrel.Mac, when it installs the downloaded update |
| Linux repository GPG key | The user's package-manager keyring | apt / dnf / zypper, only when allowUnverifiedLinuxPackages is false |
Because the anchor travels inside the previous release, you cannot simply start signing with a new key: installs that trust only the old anchor would reject everything you publish from that point on. Rotation is therefore always a transition window: you first ship a release that installs the new anchor while still passing the old check, wait for it to reach your users, and only then switch.
The bridge release
Throughout this page, a bridge release is an ordinary release with one property: it is verified with the old key and carries the new trust anchor. Users who install it will accept updates signed with the new key; users who never install it stay pinned to the old key. Where the anchor is a list (the manifest trust list, Windows publisherName), a bridge release simply lists both old and new, and the switch itself needs no flag day. The remaining sections describe what "carries the new anchor" means for each mechanism, and how long you may need to keep the old key alive.
:::tip[Plan the window before you need it]
Rotation is much calmer when it is routine. Decide in advance how you will measure adoption of a bridge release (download counts on your update server, telemetry, or simply time elapsed against your stagingPercentage schedule) and what you will do with installs that never update.
:::
Rotating the update-manifest signing key (Ed25519)
electron-builder signs latest*.yml with one or more Ed25519 private keys and embeds the matching public keys into app-update.yml; electron-updater verifies the manifest against that list before downloading anything. See Signed Update Manifests for the feature itself. Four facts drive the rotation procedure:
- An install trusts a LIST of keys.
updateManifestPublicKeyinapp-update.ymlis either a single public key or a list of them. A manifest is accepted when any listed key validates any signature it carries. Verification is fail-closed as soon as at least one key is configured — and since v27 configures one by default, treat fail-closed as the normal state. electron-updater never fetches replacement keys from the update server. - A manifest may carry several signatures. Every configured signing key signs each
latest*.yml; the result is written as asignatureslist (one{ keyId, signature }entry per key) plus the legacy singlesignaturefield holding the first key's signature.keyIdis the hex SHA-256 of the public key's SPKI DER encoding (printed bycreate-update-key), so you can see at a glance which keys signed a release. - The private keys are resolved from, in order:
updateManifest.signingKey→updateManifest.signingKeyFile→ELECTRON_BUILDER_UPDATE_SIGN_KEY→ELECTRON_BUILDER_UPDATE_SIGN_KEY_FILE. The first source that is set wins, but that source may hold several keys: config values accept an array, a PEM value may contain several concatenated-----BEGIN PRIVATE KEY-----blocks, andELECTRON_BUILDER_UPDATE_SIGN_KEY_FILEaccepts several paths joined with the OS path delimiter (:on Linux/macOS,;on Windows). RelativesigningKeyFilepaths resolve against the project directory; relativeELECTRON_BUILDER_UPDATE_SIGN_KEY_FILEpaths against the current working directory. SettingupdateManifesttofalsedisables signing outright and short-circuits this whole ladder, environment variables included. - The embedded trust list is either explicit or derived. If
updateManifest.publicKeyis set (string or array) it is embedded as-is; otherwise the public half of every signing key is derived and embedded. This lets you trust a key before you sign with it.
:::note[Platform blocks replace, they don't merge]
A platform-specific updateManifest block (for example linux.updateManifest) replaces the top-level updateManifest block for that platform rather than being merged with it. If you set publicKey at the top level but also have a per-platform block, put publicKey in the per-platform block too.
The same replacement rule makes linux.updateManifest: false an opt-out for Linux alone, leaving a top-level signing config in force for the other platforms. A platform value of null is not an opt-out — it falls back to the top-level block.
:::
Recommended steady state: always trust the next key
The cheapest rotation is the one you prepared for. Generate the next key today, keep its private half offline, and embed both public keys in every release:
# electron-builder.yml
updateManifest:
publicKey:
- | # CURRENT key — the one CI signs with
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA...
-----END PUBLIC KEY-----
- | # NEXT key — private half stored offline, not yet used
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA...
-----END PUBLIC KEY-----Every install in the field then already trusts a successor. If the current key leaks, you can start signing with the next key immediately: no bridge release, no waiting for adoption. Rotation becomes "promote next to current, generate a new next", and the bridge release described below collapses into an ordinary release.
Planned rotation, step by step
1. Generate the new keypair. Run this on a trusted machine, not in a shared CI log:
npx electron-builder create-update-key --out ./update-private-key.new.pemThe private key is written with mode 0600; the public key and its key id are printed to stdout. Store the private key as a new CI secret alongside the old one. Do not replace the old secret yet.
2. Ship the bridge release: sign with BOTH keys, trust BOTH keys. Provide both private keys to the build. The trust list is derived automatically, so no publicKey config is needed:
# two files, joined with the OS path delimiter (":" on Linux/macOS, ";" on Windows)
ELECTRON_BUILDER_UPDATE_SIGN_KEY_FILE=/run/secrets/update-key.old.pem:/run/secrets/update-key.new.pem electron-builder --publish always
# or both PEMs concatenated in one variable
ELECTRON_BUILDER_UPDATE_SIGN_KEY="$(cat update-key.old.pem update-key.new.pem)" electron-builder --publish alwaysPut the old key first: its signature also fills the legacy signature field, which is what installs built before trust lists existed verify. The resulting latest*.yml carries two entries in signatures; installs in the field verify it with the old key, and after updating trust [old, new]. Nothing else about the release needs to change.
3. Keep dual-signing for your support window. Every release you publish while both keys are configured is a bridge release: old installs verify it via the old signature, updated installs via either. How long to keep the old key depends on how far back you support updating from; measure adoption of the first dual-signed release and decide.
4. Drop the old key. Remove the old private key from the environment. From this release on, manifests are signed by the new key only and app-update.yml trusts only the new key. Installs that took any dual-signed release verify them; installs that skipped the whole window fail with ERR_UPDATER_MANIFEST_SIGNATURE_INVALID and stop updating.
5. Deal with stragglers. Installs older than the first dual-signed release trust only the old key. Your options are:
- Accept it and tell users on very old versions to reinstall from your download page.
- Keep dual-signing longer, or serve them a separate feed (for example a
latest-legacy.ymlon a differentchannel) whose manifests are still signed with the old key. This only works if the old builds can be steered there (autoUpdater.channel,setFeedURL, or a feed URL you controlled at the time). - Runtime override. Newer app code can set
autoUpdater.updateManifestPublicKeyto a list explicitly; this overrides the value fromapp-update.yml, which helps if the trust list must change without rebuildingapp-update.yml, but it does not help installs that are already in the field.
6. Retire the old key. Once no feed is signed with it, delete the old private key from CI. Keep the public key and its key id on file for auditing.
If the private key is compromised
An attacker holding a private key that installs still trust can produce a latest*.yml those installs will accept, provided they can also place it (and a payload) on your update server or intercept the connection. A trust list does not protect against the leaked key itself: as long as an install lists that key, a signature made with it verifies. The remedy is to get the leaked key out of the trust list on every install, and to stop signing with it, as fast as possible.
- Lock down the feed first. Revoke the publish credentials (
GH_TOKEN, S3/R2 keys, and so on) and audit the manifests currently served. The manifest signature protects the metadata, but only if the storage it is served from has not already been overwritten. - If installs already trust a successor key (the recommended steady state above): switch signing to that key now, embed
[successor, new-next], and never sign with the leaked key again. Every install that trusts the successor is protected from this point on. - If they do not: ship a bridge release immediately. It must still be signed with the compromised key — that is the only key the installs trust — so sign with
[compromised, new]and publish from a clean pipeline; you are racing the attacker. Follow with a release signed by the new key only as soon as adoption allows. - Remove the leaked key from the trust list in the very next release (stop providing its private key; if you list keys explicitly, delete its entry). Until an install has taken that release it remains exposed to the leaked key; verification is otherwise fail-closed, so once it has, anything the attacker signs is rejected with
ERR_UPDATER_MANIFEST_SIGNATURE_INVALID, and unsigned manifests withERR_UPDATER_MANIFEST_NOT_SIGNED.
Key storage
Never commit private keys.
updateManifest.signingKeyexists for completeness; in practice useELECTRON_BUILDER_UPDATE_SIGN_KEY(PEM contents) orELECTRON_BUILDER_UPDATE_SIGN_KEY_FILE(paths to mounted secret files) from your CI secret store.Prefer the
_FILEvariant where your CI can mount secrets as files; it keeps the keys out of process listings and environment dumps.If a private key lives in an HSM or KMS that signs on your behalf, electron-builder cannot call it directly. Sign
latest*.ymlin a post-publish step of your own (append an entry tosignatureswith the matchingkeyId, or setsignature, over the payload fromcanonicalizeForSigninginbuilder-util-runtime), and list the public key inupdateManifest.publicKeyso it is embedded. electron-builder warns at build time when none of the keys it signs with is in an explicitpublicKeylist, since such a release cannot verify its own manifests.A
publicKeyalone does not satisfy the v27 signing requirement, so this workflow takes one of two routes:- Build without a publish policy. The requirement only warns there, and electron-builder writes the unsigned
latest*.ymllocally; your step signs it and publishes the artifacts and the manifest. - Keep publishing, with
publishAutoUpdate: falseon every publish provider. electron-builder uploads the artifacts and embeds thepublicKey, but writes nolatest*.ymlat all, so your step has to generate the manifest as well as sign and upload it.
- Build without a publish policy. The requirement only warns there, and electron-builder writes the unsigned
Pre-signed download URLs (
files[].url,files[].blockMapUrl,packages.<arch>.path) are covered by the signature, so a step that writes them intolatest*.ymlhas to run before the manifest is signed — or sign it again afterwards, which needs the private key in that step. A server that mints them per request cannot serve a signed manifest.Use one key per app (or per release channel if channels are operated by different teams). Sharing a key across unrelated apps means one compromise affects all of them.
Windows: code-signing certificate rotation
Two things happen when a Windows certificate changes: the OS and SmartScreen see a new signer (reputation may need to be rebuilt for an OV certificate), and electron-updater checks the new signature against the publisher name embedded in the app. Only the second is electron-builder's concern.
How the updater checks the certificate
When win.verifyUpdateCodeSignature is true (the default), electron-builder writes the resolved publisher name into app-update.yml as publisherName. It is either what you configured in win.sign.publisherName (a string or an array) or, when not configured, the Common Name of the signing certificate. When electron-builder cannot read the signing certificate (a custom sign hook without a certificate, or a certificate without a CN), the build fails unless publisherName is set. At update time the NSIS updater reads that value and runs Get-AuthenticodeSignature on the downloaded installer:
- The installer must carry a valid Authenticode signature.
- The signer's Subject is compared against each configured publisher name. A full Distinguished Name (
CN=..., O=..., C=...) matches when every component you listed equals the certificate's; a bareCN=value matches on the Common Name alone (with a warning asking you to configure the full DN). - If any entry matches, the update is installed. If none matches, the update is rejected.
- If
app-update.ymlhas nopublisherNameat all, verification is skipped with a deprecation warning (this fail-open behavior becomes fail-closed in v28).
The same check runs again before an install-on-next-launch installer is executed.
Renewal vs. rotation
- Renewal with the same Subject (same CA, same legal entity, same DN): the embedded publisher name still matches and existing installs update without any special release. Verify the DN is byte-for-byte identical: a changed
O=,L=, orC=component is a rotation, not a renewal, if you configured a full DN. - Rotation to a certificate with a different Subject (new CA, company rename, moving to Azure Trusted Signing, switching from OV to EV): existing installs will reject installers signed with it until they have received a bridge release.
Step by step
1. Ship the bridge release with both names. Before signing anything with the new certificate, publish a release (still signed with the old certificate) whose win.sign.publisherName lists both subjects:
win:
sign:
type: signtool # or hsm / pkcs11 / azure
publisherName:
- "CN=Old Name, O=Old Company Inc, C=US"
- "CN=New Name, O=New Company Ltd, C=GB"Installs that take this release accept updates signed by either certificate. Because publisherName is now explicit, keep the entries in sync with reality; electron-builder validates the list at build time (see below).
2. Wait for adoption, then switch the signing certificate (WIN_CSC_LINK/WIN_CSC_KEY_PASSWORD, the HSM/PKCS#11 token, or the Azure account). Keep both names in publisherName for as long as you still want installs on the bridge release to keep updating, and until you are sure no pre-bridge installs matter.
3. Remove the old name once the transition is complete.
Build-time validation
When publisherName is configured explicitly and electron-builder can read the signing certificate (a .pfx/.p12 file, a Windows certificate-store entry, or an .crt/.cer file with a CN), it fails the build with InvalidConfigurationError if none of the configured names matches the certificate's subject. This catches a wrong WIN_CSC_LINK before it produces installers that every install would reject. An array containing both old and new names passes as long as one of them matches the certificate actually in use, so the bridge configuration above never trips it. The check is skipped for custom sign hooks and for Azure Trusted Signing (no local certificate to compare against). Setting publisherName: null opts out of embedding entirely.
Notes per signing method
- signtool (file / store) — rotate by pointing
certificateFile/WIN_CSC_LINK(orcertificateSubjectName/certificateSha1) at the new certificate. Timestamp your signatures (rfc3161TimeStampServer, on by default) so installers signed with the old certificate stay valid after it expires. - HSM / PKCS#11 — the certificate lives on the token; rotation means a new token or key label plus an explicit
publisherNamefor PKCS#11 without an extractable certificate, an HSM/PKCS#11.crt/.cerwithout a CN, or HSM with a customsignhook and no certificate identifier. - Custom
signhook — electron-builder cannot read the certificate the hook signs with, so setpublisherName(the build fails without it, even if a certificate is configured too). It is not checked against the hook's certificate at build time. Copy the subject from a binary the hook signed (see How do you delegate code signing?); a wrong component makes installs reject every update signed with it. - Azure Trusted Signing — there is no local certificate, so
publisherNameis required and is embedded verbatim. Confirm the Subject Azure signs with before the bridge release. - Custom
verifyUpdateFileAuthenticodeSignaturefunction — if your app replaces the verifier (see the Windows target docs), the rules above are yours to reimplement. - Squirrel.Windows is not supported by electron-updater's auto-update flow, so nothing here applies to it.
See Windows Code Signing for configuration details of each method.
macOS: certificate and Team ID
On macOS, electron-updater downloads the zip artifact and hands it to the native Squirrel.Mac framework, which verifies that the update's code signature matches the running app's before it is installed. That requirement — the update must be signed by the same Apple Team ID as the installed app — is enforced by Squirrel.Mac and macOS, not by electron-builder or electron-updater, and there is no publisher-name list to widen.
- Renewing or replacing a Developer ID Application certificate within the same Team is transparent: the Team ID does not change, so Squirrel.Mac accepts the new signature. Update
CSC_LINK/CSC_KEY_PASSWORD(or the keychain identity selected viaCSC_NAME/mac.sign.identity) and keep shipping. - Changing Team ID (moving the app to a different Apple developer account) cannot be bridged through the updater: installs signed by the old team will not accept an update signed by the new one. Users must download and install the new build manually. Announce it in-app in the last release under the old team.
- Notarization is per build and does not create a trust anchor in the installed app. Rotate notarization credentials (
APPLE_ID/APPLE_APP_SPECIFIC_PASSWORD, App Store Connect API key) whenever you like. - The Ed25519 manifest signature applies to
latest-mac.ymlexactly as on other platforms, and rotates as described above.
See macOS Code Signing and Notarization.
Linux: repository GPG keys
electron-builder does not sign .deb, .rpm, or AppImage artifacts itself. What exists:
- Manifest signature. The Ed25519 signature on
latest-linux.ymlis the integrity layer electron-builder provides for every Linux target, and it rotates as described above. The downloaded file'ssha512is checked against that manifest like on every other platform. - AppImage. electron-updater performs no package-level signature check on AppImages beyond the manifest and its
sha512. - deb / rpm.
AppUpdater.allowUnverifiedLinuxPackages(defaulttrue) controls whether the package manager's own signature checks are bypassed when a downloaded package is installed. Withfalse,apt,dnf/yum, andzypperenforce their normal verification; plaindpkgperforms no signature verification at all, and a barerpminstall cannot be made to enforce it from the command line, so the updater logs a warning in those cases.
If you sign your packages with GPG and enforce it (allowUnverifiedLinuxPackages = false), the trust anchor is the public key in the user's keyring (/etc/apt/trusted.gpg.d, /etc/pki/rpm-gpg, ...). That keyring is populated by your packaging, not by electron-builder, so rotation follows the standard distribution playbook:
- Publish the new public key in your repository metadata and on your download site.
- Ship a bridge release signed with the old GPG key whose package installs the new public key into the keyring (via a post-install script or a keyring package dependency). Users who install it now trust both keys.
- Switch package signing to the new key after adoption; installs that skipped the bridge will hit a package-manager signature error and need a manual key import or reinstall.
- Remove the old public key from the keyring in a later release.
Rotation checklist
Use this as a template for a rotation ticket.
Before
- Generate the new key/certificate on a trusted machine; store it as a new secret — do not overwrite the old one yet.
- Decide how you will measure adoption of the bridge release and how long the window lasts.
- Decide what happens to installs that never take the bridge (reinstall, legacy feed).
Bridge release (verified with the old key, carries the new anchor)
- Ed25519: sign with
[old, new]private keys (old first); the derived trust list embeds both. If you list keys explicitly,updateManifest.publicKey=[old, new]. - Windows:
win.sign.publisherName=[old subject, new subject]; sign with the old certificate. - Linux GPG (if enforced): package installs the new public key; sign with the old GPG key.
- macOS: nothing to bridge for a same-Team certificate change; announce a Team ID change in-app.
- Verify the built
app-update.ymllists both keys underupdateManifestPublicKey/ bothpublisherNameentries, andlatest*.ymlhas twosignaturesentries, before publishing. - Never reach for
updateManifest: falseto get a rotation release out: it is not a rotation tool, and it produces manifests that every install carrying a key will refuse.
Switch
- Point CI at the new private key / certificate.
- Ed25519: keep dual-signing for the support window, then remove the old private key (and its
publicKeyentry, if explicit). Consider embedding the next key already. - Windows: keep both names until the window closes.
- Confirm an install on the bridge release updates successfully from the first release signed with the new key.
After
- Remove the old subject from
publisherName, the old GPG key from the keyring. - Delete the old private key / certificate from CI; keep public halves (and the Ed25519 key id) for audit.
- Record the rotation date and the last version signed with the old key.
Related
- Signed Update Manifests — the Ed25519 manifest signing feature
- Auto Update — updater behavior and options, including
allowUnverifiedLinuxPackages - Security & Hardening — overview of update security controls
- Code Signing — certificates and environment variables per platform
Features Multi Platform Build (`website/docs/features/multi-platform-build.md`)
Multi Platform Build
:::info
Don't expect that you can build an app for all platforms on one platform.
:::
- If your app has native dependencies, they can only be compiled on the target platform unless prebuild is used. Most node modules don't provide prebuilt binaries.
- macOS Code Signing works only on macOS. Cannot be fixed.
For CI workflow examples across all platforms, see GitHub Actions.
macOS
All required system dependencies (except rpm) are downloaded automatically on demand on macOS 10.12+.
To build rpm:
brew install rpmLinux
You can use Docker to avoid installing system dependencies.
To build in distributable format:
sudo apt-get install --no-install-recommends -y libopenjp2-toolsTo build rpm:
sudo apt-get install --no-install-recommends -y rpm
# or
sudo yum install rpm-buildTo build pacman:
sudo apt-get install --no-install-recommends -y bsdtarTo build snap (only needed with custom stage packages):
sudo snap install snapcraft --classic
sudo snap install multipass --beta --classicBuild for Windows on Linux
Docker (electronuserland/builder:wine) is recommended to avoid installing system dependencies.
- Install Wine 2.0+ — see WineHQ Binary Packages.
- Install Mono 4.2+ if you want to use Squirrel.Windows (NSIS doesn't require Mono).
Build 32-bit from a 64-bit Machine
sudo apt-get install --no-install-recommends -y gcc-multilib g++-multilib:::warning[Electron 44 removed 32-bit Windows/Linux]
Windows ia32 and Linux armv7l builds require electronVersion <= 43.x — Electron 44 removed them. On Electron 44+ electron-builder fails fast with a configuration error (a warning if a custom electronDist/mirror is set). The v43 series is supported until January 2027. See Build Architectures.
:::
Docker
Build Linux or Windows targets on any platform using Docker.
:::warning
You cannot build for Windows using Docker if your app has native dependencies that don't use prebuild.
:::
:::note
Do not use Docker Toolbox on macOS. Only Docker for Mac works.
:::
Build on a Local Machine
docker run --rm -ti \
--env-file <(env | grep -iE 'DEBUG|NODE_|ELECTRON_|YARN_|NPM_|CI|CSC_|GH_|GITHUB_|BT_|AWS_|STRIP|BUILD_') \
--env ELECTRON_CACHE="/root/.cache/electron" \
--env ELECTRON_BUILDER_CACHE="/root/.cache/electron-builder" \
-v ${PWD}:/project \
-v ${PWD##*/}-node-modules:/project/node_modules \
-v ~/.cache/electron:/root/.cache/electron \
-v ~/.cache/electron-builder:/root/.cache/electron-builder \
electronuserland/builder:wineThen run yarn && yarn dist inside the container.
:::tip
If you don't need to build for Windows, use electronuserland/builder — Wine is not included in that image.
:::
Provided Docker Images
:::tip
Lock your FROM to a specific date-tag (e.g. builder:24-07.23) or digest rather than latest, to prevent unexpected toolset upgrades.
:::
:::note[v27 requires Node.js >=22.12]
The images are now published in Node.js 22 and 24 flavors (e.g. builder:22, builder:24). The old Node.js 20 tags do not satisfy v27's minimum runtime — use a 22/24 tag. See the tags list.
:::
| Image | Contents |
|---|---|
electronuserland/builder or electronuserland/builder:24 |
Node.js 24 (also available as :22) + Linux build dependencies. Use for Linux-only targets. |
electronuserland/builder:wine |
Wine + Node.js. Use for Windows targets. |
electronuserland/builder:wine-mono |
Mono for Squirrel.Windows. |
electronuserland/builder:wine-chrome |
google-chrome-stable + xvfb. Use for headless Electron testing. |
electronuserland/builder:base |
System dependencies only. Not meant for direct use. |
Images are also tagged with a date suffix -%m.%y (e.g. builder:24-07.23) for pinning. Full build script: build.sh
Features Security (`website/docs/features/security.md`)
title: "Security & Hardening"
Security & Hardening
electron-builder aims to be secure-by-default across the whole release pipeline — from how updates are verified, to how your app's contents are integrity-checked, to how artifacts are signed and published. This page is a map of those controls; each section links out to the detailed guide rather than repeating it.
:::info[v27 tightened several defaults]
Several security-relevant defaults changed in v27 — auto-update web installers, update-metadata format, and explicit publishing. If you are upgrading, read this page alongside the v27 Breaking Changes.
:::
Update security (electron-updater)
Signed update manifests (required by default). The
latest*.ymlupdate metadata is Ed25519-signed at publish time and verified by electron-updater — before any download — against a public key embedded in the app. Verification fails closed on unsigned or tampered manifests. Publishing a build that emits update metadata fails unless a signing key resolves;updateManifest: falseis the only opt-out and warns on every build. Unlike thesha2-only metadata grace period below, this one is enforced in v27. See Signed Update Manifests.Web-installer payloads are disabled by default.
AppUpdater.disableWebInstallernow defaults totrue. NSIS web installers (the small installer that downloads its full payload at install time from a manifest-supplied URL) are no longer loaded unless you opt in, because that payload may not undergo signature verification. A web-installer update is rejected withERR_UPDATER_WEB_INSTALLER_DISABLED— at download (including an update cached by a previous launch), before an install on next launch, and at install time. Installs made by annsis-webinstaller built with electron-builder v27+ opt in automatically (via theresources/package-typemarker); setautoUpdater.disableWebInstaller = falseyourself only for web-installer installs without that marker (built before v27, or with a custom script) or when switching an app fromnsistonsis-web. Cached web updates are re-verified against the freshly fetched manifest, and thensis-webinstaller verifies the package electron-updater passes to it against the packages built with it (see Web Installer). Signed update manifests cover the web package's path, SHA-512 and size, so don't setupdateManifest: falsefornsis-webapps.Update credentials stay on the feed's origin. The credential headers from
requestHeaders/addAuthHeader(headers such asAuthorization, the same set that is removed on a cross-origin redirect) and the query string of the feed URL are only sent to the update feed's origin. A download on another origin, including the same host on another port or overhttpinstead ofhttps, is requested without them. See Auto Update.Linux package signatures.
AppUpdater.allowUnverifiedLinuxPackages(defaulttrue) preserves historical behavior, since electron-builder does not sign Linux packages itself. Set it tofalseto enforce GPG signature checks when installing.deb/.rpmauto-updates on package managers that support verification.Modern, verified update metadata. Generated
latest*.ymltargets thefiles[]format and the defaultelectronUpdaterCompatibilityis now>=2.16. Update metadata validated only by the legacy SHA-256sha2checksum is deprecated.Windows update signature verification. On Windows, downloaded NSIS updates are Authenticode-verified before they are applied.
win.verifyUpdateCodeSignature(defaulttrue) embeds your signingpublisherNameintoapp-update.yml, and electron-updater checks each downloaded update against the certificate subject. Supply an array ofpublisherNamevalues when rotating certificates so updates signed by either the old or new certificate still verify.
:::warning[sha2-only metadata is rejected in v28]
v27 is a grace period for sha2-only update metadata. In v28, it is rejected. Confirm your updates use modern files[] metadata before upgrading to v28.
:::
See also: Auto Update · Update metadata & compatibility
Package integrity
Two independent mechanisms protect the code inside your packaged app:
ASAR integrity. When your app is packaged into an
app.asararchive (the default), electron-builder embeds integrity hashes so Electron can detect a tampered or swapped archive at runtime. It is on by default;asar.disableIntegrity: truedisables it (not recommended). See the ASAR options for the v27 config shape.Electron Fuses. Fuses are "magic bits" flipped at package time — before code signing — that disable risky Electron features for the entire app (e.g.
runAsNode,enableNodeCliInspectArguments,enableNodeOptionsEnvironmentVariable). PairingenableEmbeddedAsarIntegrityValidationwithonlyLoadAppFromAsarbinds the app to its verified ASAR. Because the bits are flipped before signing, the OS (Gatekeeper / App Locker) is responsible for ensuring they can't be flipped back.
See also: Adding Electron Fuses
Code signing & notarization
Signing proves your app's identity and lets the OS confirm it hasn't been tampered with since it was signed. On macOS, notarization is additionally required for Gatekeeper when distributing outside the Mac App Store. electron-builder discovers signing credentials from the environment and signs automatically when they are present.
- macOS signing (Developer ID,
mac.sign) — macOS Signing - Windows signing — signtool, HSM, PKCS#11, and Azure Trusted Signing via the
win.signdiscriminated union — Windows Signing - Apple notarization — Notarization
See also: Code Signing overview
Safe publishing
v27 removed implicit --publish. Earlier versions could auto-publish based on the presence of CI tag environment variables, git tags, or npm lifecycle events — which risked accidentally exposing secrets or shipping unfinished releases. v27 never publishes unless you ask it to: pass --publish <always|onTag|onTagOrDraft|never> explicitly in your release scripts, or set the publish option in your configuration.
electron-builder --publish always # explicit — required in v27See also: Publish configuration · Implicit --publish removed
Update feed ownership
When a GitHub or Bitbucket publish configuration omits owner/repo, electron-builder fills them in from the repository info (package.json repository, CI environment variables, then .git/config). The result becomes the publish/update destination and, for auto-update-capable targets, is written to app-update.yml inside the packaged app, so it is the update feed every installed copy keeps using — long after the build machine is gone. A build reports the repository it resolved to and the source it came from (info level for package.json repository, warn level for CI environment variables or .git/config), so read that line and confirm it is correct:
• update feed inferred from repository info; it will be used as the publish/update destination
(written to app-update.yml in auto-update-capable targets) - specify it explicitly to be sure
it stays under your control reason=owner and repo not specified in the publish configuration
source=.git/config provider=github owner=my-org repo=my-appThat name has to stay yours. GitHub frees an owner or repository name for re-registration as soon as it is renamed, transferred or deleted, and whoever claims it next can publish a release that installed copies of your app will download and run. Set owner and repo explicitly rather than relying on detection, and do not retire the namespace while builds are still in the field.
See also: GitHub repository detection
Toolset integrity
The external build toolsets electron-builder downloads (NSIS, winCodeSign, AppImage, Wine, FPM, …) are checksum-verified after download and cached, so a corrupted or substituted bundle is rejected. When you supply a custom toolset from a remote url, a checksum is required; a local file:// directory is used as-is and needs no checksum.
{
"build": {
"toolsets": {
"nsis": {
"url": "https://example.com/my-nsis-bundle.tar.gz",
"checksum": "sha256:abc123…"
}
}
}
}See also: Toolsets · Custom toolset bundles
Features Signed Update Manifests (`website/docs/features/signed-update-manifests.md`)
title: "Signed Update Manifests"
Signed Update Manifests
electron-updater always verifies the sha512 checksum of a downloaded artifact against the update manifest (latest.yml / latest-mac.yml / latest-linux.yml). That protects the artifact, but the manifest itself is only protected by the transport (HTTPS) and the storage it is served from — anyone who can modify the file on your update server can point clients at an arbitrary payload with a matching checksum.
Signed update manifests close that gap: electron-builder can sign each latest*.yml with one or more Ed25519 private keys at publish time, and electron-updater verifies the signature — before anything is downloaded — against a list of trusted public keys baked into the app at build time. Signing uses Node's built-in crypto; no additional dependencies or external tools are involved.
Signing is required: publishing a build that emits auto-update metadata fails unless a signing key resolves, and updateManifest: false is the only way to opt out. See Opting out.
Verification remains backward compatible in both directions:
- Apps built before a public key existed ignore the
signature/signaturesfields in the manifest (a one-time warning notes that verification is disabled). - Apps built with a public key fail closed: an unsigned or tampered manifest aborts the update check before any download starts.
:::warning[Full protection arrives one release after adoption]
Installs already in the field carry whatever app-update.yml they shipped with. Those built before you adopted signing have no trusted key, so they keep accepting unsigned manifests — signing a release does not retroactively protect the installs that will consume it. The installs protected by a signed manifest are the ones built after the public key was embedded.
:::
Quick setup
1. Generate a keypair (once per app or release channel):
npx electron-builder create-update-keyThis writes update-private-key.pem (mode 0600) to the current directory (use --out to choose the location) and prints the matching public key and its key id (the hex SHA-256 of the public key's SPKI DER encoding, which appears as keyId next to each signature in the manifest) for reference. Store the private key as a CI secret — it never needs to be committed anywhere.
2. Provide the private key at build/publish time, preferably via an environment variable in CI:
# either the PEM content itself…
ELECTRON_BUILDER_UPDATE_SIGN_KEY="$(cat update-private-key.pem)" electron-builder --publish always
# …or a path to the key file
ELECTRON_BUILDER_UPDATE_SIGN_KEY_FILE=/run/secrets/update-private-key.pem electron-builder --publish alwaysNo configuration block is required — the environment-variable route is the whole setup. Every generated latest*.yml gains a base64 signature field plus a signatures list (see What is written), and the derived public key is embedded into the app's app-update.yml as updateManifestPublicKey automatically.
A publishing build with no resolvable key fails instead — at build start, before anything is packed or uploaded, so none of that build's artifacts are published:
auto-update manifests must be signed, but no Ed25519 signing key was found for mac. Generate one with
`electron-builder create-update-key`, then supply it via the ELECTRON_BUILDER_UPDATE_SIGN_KEY (PEM contents)
or ELECTRON_BUILDER_UPDATE_SIGN_KEY_FILE (path) environment variable, or via updateManifest.signingKey /
updateManifest.signingKeyFile in the build configuration. To publish unsigned manifests anyway, opt out
explicitly with `updateManifest: false`.A build without a publish policy only warns, so local builds keep working while your release pipeline is the thing that has to hold the key.
3. That's it. electron-updater picks the public key up from app-update.yml and enforces verification from the first update check.
Configuration reference
Instead of (or in addition to) the environment variables, you can configure signing in the build configuration — at the top level, or per platform (e.g. under linux or win) to sign only some platforms' manifests:
updateManifest:
# Ed25519 private key(s), PEM (PKCS#8). Secret — prefer ELECTRON_BUILDER_UPDATE_SIGN_KEY in CI.
# A string (which may contain several concatenated PEM blocks) or an array of strings.
signingKey: null
# Path(s) to file(s) containing private key(s). Alternative to signingKey. String or array.
# Relative paths resolve against the project directory.
signingKeyFile: null
# Public key(s) (PEM or base64 SPKI) embedded into app-update.yml as the updater's trust list.
# Optional — derived automatically from the private key(s) when omitted. String or array.
publicKey: nullThe whole option may instead be set to false, the only opt-out:
updateManifest: false # publishes UNSIGNED manifests; warns on every buildKey resolution order is: signingKey → signingKeyFile → ELECTRON_BUILDER_UPDATE_SIGN_KEY → ELECTRON_BUILDER_UPDATE_SIGN_KEY_FILE. The first source that is set wins, but that source may provide several keys:
| Source | Several keys |
|---|---|
signingKey / ELECTRON_BUILDER_UPDATE_SIGN_KEY |
concatenate the PEM blocks (cat a.pem b.pem), or use an array in config |
signingKeyFile |
an array of paths in config |
ELECTRON_BUILDER_UPDATE_SIGN_KEY_FILE |
join the paths with the OS path delimiter: a.pem:b.pem on Linux/macOS, a.pem;b.pem on Windows |
A relative signingKeyFile path resolves against the project directory (where the build configuration lives), like every other path in the configuration; a relative ELECTRON_BUILDER_UPDATE_SIGN_KEY_FILE resolves against the current working directory, as environment-variable paths usually do.
Every key must be a distinct Ed25519 key; duplicates and other key types fail the build. When several keys are configured, each manifest is signed by all of them and the trust list embeds all of their public keys — this is the dual-signing used during key rotation.
An explicit publicKey takes precedence over derivation and is embedded as-is. Use it to trust keys you do not (yet) sign with — for example the next key ahead of a rotation. If none of the keys the build signs with is in an explicit publicKey list, electron-builder logs a warning: installs of that release could not verify its own manifests, which is only ever intended for a deliberate bridge release.
A publicKey on its own does not satisfy the signing requirement — electron-builder still has no key to sign with, so a publishing build fails. An HSM/KMS-style signer that signs latest*.yml in a later step of its own therefore either builds without a publish policy, where the requirement only warns and the unsigned manifest is written locally for it to sign, or keeps publishing with publishAutoUpdate: false on every publish provider — electron-builder then uploads the artifacts and embeds the publicKey, but writes no latest*.yml, so the external step has to generate the manifest too. See Key storage.
At runtime you can also set the trust list on the updater directly; it overrides the value from app-update.yml:
import { autoUpdater } from "electron-updater"
autoUpdater.updateManifestPublicKey = "-----BEGIN PUBLIC KEY-----\n…" // or an array of keysOpting out
Set updateManifest: false — at the top level, or under a platform key — to publish unsigned manifests. This is the only opt-out, and every build that produces a manifest logs a warning naming the consequence.
updateManifest: falseTwo properties are worth knowing:
- It short-circuits the environment variables. A leftover
ELECTRON_BUILDER_UPDATE_SIGN_KEYin CI cannot re-enable signing behind the back of the opt-out, sofalsemeans unsigned everywhere, deterministically. - A platform value wins over the root, so
linux: { updateManifest: false }opts out for Linux only while a root signing config still covers the other platforms — and the converse works too. A platform value ofnull, on the other hand, falls back to the root rather than opting out.
The requirement is also waived, without any configuration, where no manifest is emitted at all: a build whose publish providers all set publishAutoUpdate: false, a build whose targets write no update info (for example only snap, flatpak, MSI/MSIX, portable, mas/pkg or plain archives), and any build without a publish policy (which only warns).
:::danger[Opting out is not reversible for a release line]
Once a release embeds a public key, its installs are fail-closed: an unsigned manifest is rejected with ERR_UPDATER_MANIFEST_NOT_SIGNED and those installs stop updating entirely. Do not switch to updateManifest: false after shipping signed manifests, and make sure every pipeline that can publish a release — including a hotfix built from a laptop — has the key.
:::
Legitimate reasons to opt out: an app that does not auto-update at all. A pipeline that signs latest*.yml itself in a later step should not opt out — false also drops updateManifest.publicKey, so installs would embed no trust list and never verify those signatures; see Key storage for its two routes.
What is written
app-update.yml gains updateManifestPublicKey: a single string when one key is trusted (unchanged from the single-key format), a YAML list when several are.
Each signed latest*.yml gains:
signature: b0Qk… # legacy single field: the FIRST signing key's signature
signatures:
- keyId: 3f9c…e1 # hex SHA-256 of the signing key's public key (SPKI DER), as printed by create-update-key
signature: b0Qk… # same value as `signature` above
- keyId: a71d…04 # one more entry per additional signing key
signature: Xr2w…Both fields are always written, even with a single key, so the manifest shape does not change when a second key is added. Each entry signs the same payload; the keyId lets an updater holding several trusted keys pick the matching signature directly, and lets you tell at a glance which keys signed a release.
What is signed
Every signature covers the integrity-critical fields of the manifest: version, stagingPercentage, minimumSystemVersion (its absence is signed too, so an OS-version gate cannot be added or removed after signing), every file entry's url, sha512, and size (and its blockMapUrl when present), and — for the NSIS web installer — every packages entry's path, sha512, size, blockMapSize, and isAdminRightsRequired (all in a canonical, order-independent form). Cosmetic fields — releaseName, releaseNotes, and releaseDate — are not covered, so they remain editable after publishing without invalidating the signature. The signature/signatures fields themselves (including keyId) are not part of the signed payload, so signatures can be added or removed without invalidating the others.
files[].isAdminRightsRequired is intentionally not signed either. It only decides how electron-updater launches the already hash-verified installer (directly, or through elevate.exe), so tampering with it cannot change what gets installed — at worst it causes an unexpected UAC prompt — while leaving it unsigned lets you correct a misconfigured value in latest.yml after publishing without re-signing.
The canonical form is injective: every string value is JSON-quoted (so a value can never contain an unescaped newline or tab and cannot impersonate another record), numbers are written bare, and an absent optional field is encoded differently from an empty one. Two manifests therefore produce the same signed bytes only if every covered field is identical.
A signed manifest must also have the shape the signature is meant to protect. Before trying any key, the updater rejects a manifest whose files list is empty or missing, whose version, files[].url/sha512, minimumSystemVersion or packages entries are not plain strings/numbers, or whose signed string fields contain control characters, with ERR_UPDATER_MANIFEST_SIGNATURE_INVALID naming the problem. In particular, the deprecated top-level path/sha512 fields (an unsigned mirror of files[0]) are never used to download a verified update. electron-builder applies the same checks when signing, so a build cannot produce a signed manifest that its updaters would refuse.
Verification behavior in electron-updater
When at least one public key is configured (embedded or set at runtime), verification is enforced on every update check, for every provider. The manifest is accepted when any trusted key validates any of its signatures (signatures entries tagged with that key's id are tried first, then the untagged legacy signature):
| Manifest state | Result |
|---|---|
| At least one signature validates against a trusted key | Update proceeds |
No signature and no signatures |
Error ERR_UPDATER_MANIFEST_NOT_SIGNED |
| Signed, but tampered or signed only by keys the install does not trust | Error ERR_UPDATER_MANIFEST_SIGNATURE_INVALID |
Both errors are emitted through the updater's regular error event and abort the update before any download begins.
:::warning[The hazard is turning it off, not turning it on]
Verification is enforced by the installed app, and since signing is on by default there is nothing to roll out — your first build already embeds the public key and signs its manifests. Older installs without the key simply skip verification. What matters from then on is that every future manifest is signed: publishing an unsigned one makes every install that carries the key refuse the update.
:::
Key rotation
An install trusts the list of public keys embedded in its app-update.yml, and a manifest can carry a signature from each of several keys. Rotation therefore needs no flag day:
- Bridge release(s): configure both the old and the new private key (old first, so it also fills the legacy
signaturefield), e.g.ELECTRON_BUILDER_UPDATE_SIGN_KEY_FILE=old.pem:new.pem. Manifests are now dual-signed andapp-update.ymltrusts[old, new]. Installs in the field verify via the old signature; once updated they trust both keys. - Keep dual-signing for as long as you want installs that predate the first bridge release to be able to update.
- Drop the old key from the environment. Manifests are signed by the new key only; every install that took a bridge release keeps verifying. Installs that never did stop updating (
ERR_UPDATER_MANIFEST_SIGNATURE_INVALID) and must be reinstalled or served from a separate feed still signed with the old key.
Better still, embed the next key before you need it: set updateManifest.publicKey to [current, next] and keep the next private key offline. A compromise of the current key can then be answered by switching to the next key at once, with no bridge release. Note that a trust list does not protect against a leaked key itself while installs still list it — remove it from the list as fast as adoption allows. The full procedure — including compromise handling, key storage advice, and the equivalent steps for Windows, macOS, and Linux code-signing keys — is in the Key Rotation runbook.
Related: Linux package signature enforcement
Manifest signing protects the update metadata. Separately, AppUpdater.allowUnverifiedLinuxPackages (default true) controls whether the OS package manager's own GPG/signature checks are bypassed when installing .deb / .rpm auto-updates. If you sign your Linux packages, set it to false to enforce them — see Auto Update and Security & Hardening.
Migration V26 To V27 (`website/docs/migration/v26-to-v27.md`)
title: "Migrating from v26 to v27"
Migrating from v26 to v27
This is the step-by-step walkthrough for upgrading a project from v26 to v27. For the authoritative catalogue of everything that changed, see v27 Breaking Changes →.
v27 migrates the entire electron-builder ecosystem to native ES modules, raises the minimum Node.js version to 22.12.0, and hard-deletes the deprecated APIs that accumulated since v22.
:::danger[Don't upgrade without reading the breaking changes]
Every breaking change — what's removed, renamed, restructured, and what action each requires — is catalogued on the v27 Breaking Changes page. Skim it before you start so nothing surprises you.
:::
Most projects need only a Node.js version bump plus one command. The build() API is unchanged and CJS require() continues to work without any code changes on supported Node.js versions. Configuration keys that were renamed or restructured (win.sign, mac.sign, electronGet, asar, nativeModules, …) are rewritten for you automatically by migrate-schema in Step 1 — they keep the same runtime behavior. A few type exports were renamed or removed; see the breaking changes.
The upgrade is three steps:
- Run the automated migrator — rewrites your config to v27 form
- Update Node.js — to >=22.12.0
- Apply the manual steps — the few things the migrator can't do
Then walk the summary checklist.
Step 1: Run the automated migrator
Run the built-in command to apply every config-level breaking change to your project automatically:
electron-builder migrate-schema # apply changes in place
electron-builder migrate-schema --dry-run # preview without writing (alias: -n)This rewrites your electron-builder.json, electron-builder.yml, package.json (build key), or any other static config format in place. It also rewrites programmatic configs (.js/.ts/.cjs/.mjs) via an AST-located codemod that preserves comments, imports, functions, and formatting.
Without --config, it looks for a build object in package.json and then for electron-builder.{yml,yaml,json,json5,toml,js,cjs,mjs,ts} in the project root. If your config file has another name (for example one your build scripts pass as electron-builder --config my-config.cjs), pass the same path: electron-builder migrate-schema --config my-config.cjs. Pass --project-dir <dir> to specify the project root (default: current directory).
What it migrates automatically
| Config change | Before | After |
|---|---|---|
electronCompile removed |
"electronCompile": true |
(deleted) |
disableDefaultIgnoredFiles removed |
"disableDefaultIgnoredFiles": true |
(deleted) |
framework / nodeVersion / launchUiVersion removed |
"framework": "electron" |
(deleted) |
npmSkipBuildFromSource removed |
"npmSkipBuildFromSource": true |
"nativeModules": { "buildDependenciesFromSource": false } |
| Native-module options grouped | "npmRebuild": true |
"nativeModules": { "npmRebuild": true } |
nativeRebuilder renamed |
"nativeRebuilder": "parallel" |
"nativeModules": { "rebuildMode": "parallel" } |
nativeRebuilder: "legacy" removed |
"nativeRebuilder": "legacy" |
(dropped with a warning — @electron/rebuild "sequential" applies) |
appImage.systemIntegration removed |
"appImage": { "systemIntegration": "ask" } |
(deleted) |
linux.syncDesktopName removed |
"linux": { "syncDesktopName": true } |
(deleted — always synced in v27) |
| Legacy asar keys | "asar-unpack": "**/*.node" |
"asar": { "unpack": ["**/*.node"] } |
asarUnpack consolidated |
"asarUnpack": ["**/*.node"] |
"asar": { "unpack": ["**/*.node"] } |
disableSanityCheckAsar moved |
"disableSanityCheckAsar": true |
"asar": { "disableSanityCheck": true } |
disableAsarIntegrity moved |
"disableAsarIntegrity": true |
"asar": { "disableIntegrity": true } |
asar: true removed |
"asar": true |
(deleted — absence means enabled) |
Platform asarUnpack moved |
"asarUnpack": ["**/*.node"], "mac": { "asarUnpack": ["x/**"] } |
"asar": { "unpack": ["**/*.node"] }, "mac": { "asar": { "unpack": ["**/*.node", "x/**"] } } (root options merged in — a platform asar replaces the root one) |
Keys with no effect under asar: false |
"asar": false, "asarUnpack": "x" |
"asar": false |
macOS signing fields → sign |
"mac": { "identity": "Developer ID...", "hardenedRuntime": true } |
"mac": { "sign": { "identity": "Developer ID...", "hardenedRuntime": true } } |
signIgnore renamed |
"mac": { "signIgnore": ["**/*.txt"] } |
"mac": { "sign": { "ignore": ["**/*.txt"] } } |
mac.type moved |
"mac": { "type": "development" } |
"mac": { "sign": { "type": "development" } } |
gatekeeperAssess removed |
"mac": { "gatekeeperAssess": false } |
(deleted — @electron/osx-sign 2.x has no spctl --assess step) |
v26 null = unset on mac options |
"mac": { "provisioningProfile": null } |
(deleted — the v27 option rejects null; identity: null is kept) |
mac.universal fields → universal |
"mac": { "mergeASARs": true } |
"mac": { "universal": { "mergeASARs": true } } |
electronDownload → electronGet |
"electronDownload": { "mirror": "https://m/" } |
"electronGet": { "mirrorOptions": { "mirror": "https://m/" } } |
electronDownload fields with no v5 equivalent |
"electronDownload": { "strictSSL": false, "force": true } |
(dropped with a warning) |
GithubOptions.vPrefixedTagName |
"vPrefixedTagName": false |
"tagNamePrefix": "" |
GitHub empty tagNamePrefix (ignored by v26) |
{ "provider": "github", "tagNamePrefix": "" } |
{ "provider": "github", "tagNamePrefix": "v" } (keeps your tag names; warns) |
win.azureSignOptions → win.sign |
"win": { "azureSignOptions": { "endpoint": "…" } } |
"win": { "sign": { "type": "azure", "endpoint": "…" } } |
win.signtoolOptions → win.sign |
"win": { "signtoolOptions": { "certificateFile": "…" } } |
"win": { "sign": { "type": "signtool", "certificateFile": "…" } } |
| Azure extra sign keys | "win": { "azureSignOptions": { "ExcludeCredentials": "X" } } |
"win": { "sign": { "type": "azure", "additionalMetadata": { "ExcludeCredentials": "X" } } } |
win.signExecutable: false |
"win": { "signExecutable": false } |
"win": { "sign": false } |
win.signExecutable: true removed |
"win": { "signExecutable": true } |
(deleted — enabled by default) |
win.signAndEditExecutable: true removed |
"win": { "signAndEditExecutable": true } |
(deleted — always enabled in v27) |
win.signAndEditExecutable: false |
"win": { "signAndEditExecutable": false } |
"win": { "sign": false } (resource editing now always runs; warns) |
| Signing options under disabled signing | "win": { "signExecutable": false, "signtoolOptions": { … } } |
"win": { "sign": false } (the unused block is dropped; warns) |
snap → snapcraft |
"snap": { "confinement": "strict", "base": "core22" } |
"snapcraft": { "base": "core22", "core22": { "confinement": "strict" } } |
helper-bundle-id moved |
"helper-bundle-id": "com.x.helper" |
"mac": { "helperBundleId": "com.x.helper" } |
squirrelWindows.noMsi inverted |
"squirrelWindows": { "noMsi": true } |
"squirrelWindows": { "msi": false } |
Root-level directories moved |
{ "directories": { "output": "dist" } } (package.json root) |
{ "build": { "directories": { "output": "dist" } } } |
toolsets.*: null rejected |
"toolsets": { "wine": null } |
(deleted — unset resolves to "latest") |
Retired appimage pin |
"toolsets": { "appimage": "1.0.2" } |
"toolsets": { "appimage": "1.0.3" } |
It also warns without rewriting where there is no mechanical equivalent — squirrelWindows.customSquirrelVendorDir and a custom NSIS bundle in nsis.customNsisBinary / customNsisResources (see Step 3; it does move customNsisBinary.debugLogging → installerDebugLogging), a custom mac.sign signer next to sibling signing options, and, in JS/TS configs only, a platform-level asarUnpack next to root-level ASAR options — and prints advisories (never a rewrite) for an nsis-web target, nsis.perMachine or nsisWeb.perMachine set to true, a custom win.sign.sign hook without win.sign.publisherName, a generic publish configuration whose url has a query string, and a mac/mas/masDev section that names no entitlements file. See what migrate-schema does and does not do.
Each row links to its full explanation on the v27 Breaking Changes page.
Serialization caveats
- JSON5 → JSON: when
migrate-schemarewrites a.json5file it produces valid JSON (no comments, standard quoting) and prints a warning. If preserving comments matters, apply the changes manually. - TOML: the
tomlnpm package is read-only.migrate-schemadetects TOML configs, prints the required changes, and exits without writing. - YAML:
js-yamlpreserves key order when round-tripping but does not preserve comments. - Programmatic configs (
.js/.ts/.cjs/.mjs): rewritten in place when reducible to a single object literal; falls back to printing manual steps for dynamic function bodies, spreads, or computed keys, or whentypescriptis not installed. snapbase defaulting: when your oldsnapconfig has nobasefield, the tool assumes"core20"(v27's 1-to-1 migration target) and prints a warning so you can confirm or change it.
Step 2: Update Node.js
v27 requires Node.js 22.12.0 or later — the version where Node's require(esm) support was stabilized, allowing both CJS and ESM consumers to use these packages without code changes.
Update your local environment:
# nvm
nvm install 22 && nvm use 22
# fnm
fnm install 22 && fnm use 22Update CI (GitHub Actions):
- uses: actions/setup-node@v4
with:
node-version: '22'Update your package.json engines field if you declare one:
{ "engines": { "node": ">=22.12.0" } }Confirm your CI and Docker images run Node.js 22.12 or later:
FROM node:22-bookworm-slimelectron-builder's own Docker images for Linux builds have been updated and are available in both node 22 and 24 flavors: https://hub.docker.com/r/electronuserland/builder/tags
ESM/CJS — no code changes needed on Node >=22.12
// CJS require() — still works
const { build } = require("electron-builder")
// ESM import — now the preferred style
import { build } from "electron-builder"moduleResolution settings "node", "node16"/"nodenext", and "bundler" (recommended) all work. See Native ESM output for details.
Step 3: Apply the manual steps
migrate-schema handles every config-level change except squirrelWindows.customSquirrelVendorDir and a custom NSIS bundle in nsis.customNsisBinary / customNsisResources. The following require manual action — each links to its full explanation:
- Migrate off
electron-compileto a modern bundler (electron-vite / esbuild / webpack), if you usedelectronCompile. - Add
--publishexplicitly to your release scripts — auto-publish is gone. - Update-manifest signing: it is required on publishing builds — run
npx electron-builder create-update-keyonce, store the PEM as a CI secret and expose it asELECTRON_BUILDER_UPDATE_SIGN_KEY, or setupdateManifest: falseto keep publishing unsigned manifests (warned about on every build). Every pipeline that can publish a release needs the key. - Replace
devMetadata/extraMetadatain the programmatic API withconfig.extraMetadata. - Replace
--em.build/--em.directoriesCLI flags with-c/-c.directories. -
linux.syncDesktopNameis removed automatically bymigrate-schema— the behaviour is now always on. Only if you had it set tofalse: setdesktopNameto control the installed.desktopfilename (the migrator warns you). - Switch
<%= var %>→${var}in Linux maintainer scripts. - Update custom NSIS scripts that hard-code the old file-association ProgID.
- Replace
CI_BUILD_TAGwithCI_COMMIT_TAG. - Replace toolset env-var overrides (
APPIMAGE_TOOLS_PATH,ELECTRON_BUILDER_NSIS_DIR,USE_SYSTEM_WINE, …) withtoolsets.X: { url, checksum }. - Validate toolset pins — defaults now resolve to
"latest"(newest bundle); pin to"0.0.0"only if a legacy bundle is needed. - 32-bit builds:
arch: "all"now expands to x64 + arm64 (was x64 + ia32) — requestia32explicitly to keep it. Building Windowsia32/ Linuxarmv7lon Electron 44+ now fails fast; pinelectronVersionto43.xor earlier (supported until January 2027) or drop those targets. - macOS names:
productName/executableNameare now validated, not silently sanitized — if a build starts erroring on the name, choose one that needs no filename sanitization. - Bitbucket publishing: a token set without a username is now sent as Bearer auth — if
BITBUCKET_TOKENholds an app password / API token, also setBITBUCKET_USERNAMEso Basic auth is used; genuine access tokens need no username. - node_modules arch/os filtering: dependencies are now
cpu/os-filtered against the target on every build — a package whosecpu/osmismatches the target is excluded (was effectively universal-macOS-only). Re-include an intentionally cross-arch binary viaextraResources/files. - macOS DMG: the DMG
filesystemdefault is now APFS — setdmg.filesystem: "HFS+"only if you need pre-10.13 (High Sierra) compatibility. - electron-updater:
disableWebInstallernow defaults totrue— web-installer updates are rejected (ERR_UPDATER_WEB_INSTALLER_DISABLED) unless it isfalse. Installs made by a v27+nsis-webinstaller opt in automatically; setdisableWebInstaller: falseonly for web-installer installs without theresources/package-typemarker (pre-v27 installers, custom scripts) or when switching fromnsistonsis-web. Thensis-webinstaller now verifies and installs its own copy of the app package and leaves a local package file in place; setnsisWeb.allowUnverifiedAppPackage: trueonly if you use one web installer with packages of other builds. - NSIS per-machine updates: per-machine builds set
isAdminRightsRequiredin the update info — their updates are started withelevate.exe; withautoInstallEvent: "onNextLaunch", per-machine apps callinstallPendingUpdateIfAvailable()to install a pending update. - electron-updater: update credentials stay on the feed's origin — if
latest*.ymlpoints downloads at another origin that needs yourrequestHeaders/addAuthHeadercredentials or a token in the feed URL's query, serve the files from the feed origin or use pre-signed URLs. A custom provider used with credential headers must overrideProvider.feedBaseUrl(its feed URL, ornull), else downloads fail withERR_UPDATER_FEED_BASE_URL_NOT_DECLARED. - electron-updater:
latest*.ymlno longer includes legacy top-levelpath/sha512— no action for updater >=2.16; migrate any app code readinginfo.path/info.sha512toinfo.files[0], and setelectronUpdaterCompatibilityto a legacy-inclusive range only if you still ship 1.x–2.15 updaters. - Production dependencies: redundant production
dependenciesare excluded, not rejected —electron/electron-builderare excluded from the copiednode_modules(was a hard error) and theALLOW_ELECTRON_BUILDER_AS_PRODUCTION_DEPENDENCYenv var is removed (if you used it to bundleelectron-builder, overrideignoredProductionDependencieswithout that name); tune the set viaignoredProductionDependencies(e.g. add bundler-inlined deps likereact).electron-prebuilt/electron-rebuildno longer error and are NOT excluded — remove them fromdependencies(they are deprecated) or add them to the list. - Remove
squirrelWindows.customSquirrelVendorDir— the option is deleted; supply a custom Squirrel bundle viatoolsets.squirrel(aToolsetCustomobject) instead (note the different bundle layout). - Migrate any custom NSIS-binary and/or custom NSIS-resources to a
toolsets.nsisbundle —Potentially relevant for builds reliant on NSIS (nsisWeb,portable,nsis). The.customNsisBinaryand.customNsisResourcesconfig properties have been removed in favor of a new NSIS-bundle configured viatoolsets.nsis. The new toolset consolidates the removed properties and provides both the NSIS binary and resources. Configuring the removed properties fails the build.customNsisBinary.debugLoggingis nownsis.installerDebugLogging(moved bymigrate-schema); it needs a log-enabled NSIS (NSIS_CONFIG_LOG=yes), which the default toolset is not, so it requires a customtoolsets.nsisbundle. - Wine on macOS:
toolsets.winenow defaults to the host install — a macOS host that builds Windows targets needs Wine installed (brew install --cask wine-stable). - macOS entitlements: the bundled default now grants only
allow-jit— if your app loads native modules, frameworks, or plugins signed by another Team ID (or unsigned), adddisable-library-validation(andallow-unsigned-executable-memoryif needed) back inbuild/entitlements.mac.plist. - Missing dependencies: a production dependency that cannot be resolved now fails the build — fix the install or set
allowMissingDependencies. - Native binaries: a
.nodeaddon built for another platform/arch now fails the build — rebuild it for the target, or setnativeModules.verifyNativeBinariesto"warn"/false. -
extraFiles/extraResources: atothat is absolute or escapes the output dir now throws. - Windows
publisherName: is validated against the signing certificate — updatewin.sign.publisherNameafter a certificate rotation. A customwin.sign.signhook (even with a certificate configured), or a certificate without a CN, now needswin.sign.publisherNamefor builds that writeapp-update.yml(nsis/nsis-web/electronUpdaterAwareappxwith a publish config, including one inferred from a GitHubrepository); setwin.verifyUpdateCodeSignature: falseinstead only if your updates are not Authenticode-signed or you don't use electron-updater. - NSIS scripts: rename a
customInstallmodemacro to the documentedcustomInstallMode. - Linux launcher:
.desktopExecnow runs a*-launcherscript andexecutableArgsfield codes are literal — update custom.desktop/ AppArmor / MIME tooling and move%F/%Utolinux.desktop.entry.Exec. - Update channels: suffixed channels (e.g.
beta-arm64) now expand to lower channels withgenerateUpdatesFilesForAllChannels— set it tofalseto keep one file per suffixed channel. - electron-updater API:
quitAndInstalltakes an options object,autoInstallOnAppQuit→autoInstallEvent, anddownloadUpdate()resolves with{ updateFile, packageFile }— update app code. - TypeScript imports: renamed type exports (
ElectronDownloadOptions→ElectronGetOptions, …) have no compat aliases. - Plugin/custom-target authors: replace
packager.info.XandplatformSpecificBuildOptionswith the new pass-through getters.
Step 4: Custom NSIS Migration Guide
If you do do not use either .customNsisBinary or .customNsisResources configurations on nsis nsis-web or portable in your <=v26 config there are no actions required and you can skip this step.
To migrate from .customNsisBinary/.customNsisResources to a new custom unified toolsets.nsis bundle requires manual steps.
A brief overview of this breaking change is good context before you start this migration. The NSIS Toolset Bundle docs may also be useful.
**If you previously used `.customNsisBinary` in `v26` or prior:**
- Download & extract a copy of the new default NSIS Toolset Bundle from electron-builder-binaries as well as the archive you provided via
.customNsisBinary. - Depending on what OS(es) your
.customNsisBinarywas built to support, you may have any subset of these three binaries that need to be copied over.- a.
/Bin/makensis.exe - b.
/linux/makensis - c.
/mac/makensis
- a.
- The NSIS Toolset Bundle you download is a valid
toolsets.nsisbundle and provides easy locations you can drop anymakensisbinaries you into.- a.
/windows/← Copy/Bin/makensis.exeFrom your.customNsisBinaryarchive to this path to migrate - b.
/mac/x64← Copy/mac/makensisfrom a.customNsisBinaryarchive to this path to migrate - c.
/mac/arm64 - d.
/linux/x64← Copy/linux/makensisfrom a.customNsisBinaryarchive to this path to migrate - e.
/linux/arm64
- a.
- When copying your custom binaries across you should overwrite the makensis executables already present in the new-bundle.
- (optional) - As you can see, the new
toolset.nsisbundle now provides native makensis binaries for newly supported targets likeMac (arm64)&Linux (arm64). You're welcome to provide binaries for as many (or as few) Operating-System/Architecture targets as you want in your custom toolset. You only need to provide amakensis/makensis.exeexecutable for the OS & arch combos you intend to run your builds with. - (optional) - The new
toolsets.nsisis more flexible than previous.customNsisBinary/.customNsisResourcesin that the layout is customizable. The following files hook incoming makensis executions. This shim-script-layer opens up a path for custom dynamic logic to adjust binary/resources conditionally and on the fly.- a.
/makensis← Bash script, routes incoming makensis execution requests for macOS/Linux builds - b.
/makensis.cmd← Cmd script, routes incoming makensis execution requests for Windows builds running via Command Prompt - c.
/makensis.ps1← PowerShell script, may be used in the future by electron-builder to route incoming windows builds
- a.
- Include your newly created bundle in your electron builder config via
toolsets.nsisby either:- a. Providing a
file://as thetoolsets.nsis.url. - b. Similar to how
.customNsisBinary/.customNsisResourcesworked - host the bundle as a.7zfile remotely. Provide the remote url as thetoolsets.nsis.url& and provide the hash of the archive (either sha256-hex or sha512-base64) to thetoolsets.nsis.checksumproperty.
- a. Providing a
**If you previously used `.customNsisResources` in `v26` or prior:**
Download & extract a copy of the new default NSIS Toolset Bundle from electron-builder-binaries as well as the archive you provided via
.customNsisResources.For the
.customNsisResources, all 3rd party resource .dlls were located as such:- a.
/plugins/x86-ansi/ - b.
/plugins/x86-ansi/ - c.
plugins/<Target>/.dll
- a.
The NSIS Toolset Bundle you download is a valid
toolsets.nsisbundle and provides an easy locations you can drop in your resources. The root NSISDIR (NSIS Resources Directory) is now/windows/. The move of just resources is pretty straightforward:- a.
/windows/Plugins/x86-ansi/ - b.
/windows/Plugins/x86-ansi/ - c.
plugins/<Target>/.dll
- a.
When copying your custom resources across you should overwrite the makensis executables already present in the new-bundle.
The complete common NSIS resources pulled by
makensisbinary runtime are/Stubs,/Include,/Contrib,/Plugins. Nothing outside of/pluginswas previously supported by.customNsisResources. With the new NSIS toolset, if you have any of these resources then they can go here:- a.
/windows/Plugins/ - b.
/windows/Include/ - c.
/windows/Contrib/ - d.
/windows/Stubs/ - e.
/windows/← Root NSISDIR (Nsis Resources Dir) by default
- a.
(optional) - The new
toolsets.nsisis more flexible than previous.customNsisBinary/.customNsisResourcesin that the layout is customizable. The following files configure NSISDIR (NSIS Resource Directory). This shim-script-layer opens up a path to dynamically control NSISDIR on the fly.- a.
/makensis← Bash script, routes incoming makensis execution requests for macOS/Linux builds - b.
/makensis.cmd← Cmd script, routes incoming makensis execution requests for Windows builds running via Command Prompt - c.
/makensis.ps1← PowerShell script, routes incoming makensis execution requests for Windows builds running via Powershell
- a.
Include your newly created bundle in your electron builder config via
toolsets.nsisby either:- a. Providing a
file://as thetoolsets.nsis.url. - b. Similar to how
.customNsisBinary/.customNsisResourcesworked - host the bundle as a.7zfile remotely. Provide the remote url as thetoolsets.nsis.url& and provide the hash of the archive (either sha256-hex or sha512-base64) to thetoolsets.nsis.checksumproperty.
- a. Providing a
The
toolsets.nsisbundle must carryelevate.exeat the bundle's root directory You can grab a copy one from the root of Electron Builder's default NSIS Toolset Bundle provided in electron-builder-binaries. It is used for privledge-control features by the UAC nsis plugin.For more information/guidance you can checkout the
toolsets.nsisdocumentation
Summary checklist
Run electron-builder migrate-schema first — it handles the items marked ✓ automatically.
Runtime and CI
- Node.js runtime updated to >=22.12.0
- CI
node-versionupdated to'22' -
"engines"field inpackage.jsonupdated (if declared)
Build config — auto-migrated by migrate-schema
- ✓
electronCompileremoved (if present) - ✓
disableDefaultIgnoredFilesremoved (if present; re-include specific files via afilesglob such as**/*.obj) - ✓
framework,nodeVersion,launchUiVersionremoved (if present) - ✓
npmSkipBuildFromSource→nativeModules.buildDependenciesFromSource(if present) - ✓
buildDependenciesFromSource,nodeGypRebuild,npmRebuildmoved intonativeModules;nativeRebuilder→rebuildMode - ✓ Legacy asar keys +
asarUnpack→asar.unpack(platform-level too);disableSanityCheckAsar→asar.disableSanityCheck;disableAsarIntegrity→asar.disableIntegrity;asar: trueremoved - ✓ macOS signing fields →
mac.sign.*;signIgnore→sign.ignore(alsomas/masDev) - ✓
mac.universalfields →mac.universal.* - ✓
electronDownload→electronGet(verify droppedcache/customDir/customFilename/strictSSL/platform/arch/version/force) - ✓
appImage.systemIntegrationremoved (if present) - ✓
linux.syncDesktopNameremoved (if present; always synced in v27 — setdesktopNameif you had itfalse) - ✓
GithubOptions.vPrefixedTagName→tagNamePrefix(if present) - ✓
win.azureSignOptions/win.signtoolOptions→win.sign;win.signExecutable/win.signAndEditExecutablehandled - ✓
snaprestructured tosnapcraft(verify the assumedbaseif none was set) - ✓
helper-bundle-id→mac.helperBundleId;squirrelWindows.noMsi→msi; root-leveldirectories→build.directories - ✓
toolsets.*: nullremoved;toolsets.appimage: "1.0.2"→"1.0.3";nativeRebuilder: "legacy"dropped
Runtime defaults & auto-update
- DMG
filesystemdefault is now APFS — setdmg.filesystem: "HFS+"only for pre-10.13 macOS compatibility - macOS default entitlements now grant only
allow-jit— addallow-unsigned-executable-memory/disable-library-validationback inbuild/entitlements.mac.plist(and.inherit.plistfor nested binaries) only if your app loads foreign-signed or unsigned binaries; MAS builds withoutbuild/entitlements.mas.plistnow get osx-sign's sandboxed default - electron-updater
disableWebInstallerdefaults totrue— web-installer updates are rejected unless it isfalse; v27+nsis-webinstalls opt in automatically, setdisableWebInstaller: falsefor older web-installer installs or when switching fromnsistonsis-web; thensis-webinstaller verifies its app package (nsisWeb.allowUnverifiedAppPackageopts out) - A code-signed Windows build that writes
app-update.ymlneeds a publisher name — setwin.sign.publisherNamefor a customwin.sign.signhook or a certificate without a CN, orwin.verifyUpdateCodeSignature: falseif your updates are not Authenticode-signed - NSIS per-machine builds set
isAdminRightsRequired— their updates are started withelevate.exeand not installed automatically at launch; withautoInstallEvent: "onNextLaunch", callinstallPendingUpdateIfAvailable() - Update credentials stay on the feed's origin —
requestHeaders/addAuthHeadercredentials and the feed URL's query are not sent to downloads on another origin -
latest*.ymldrops legacy top-levelpath/sha512; defaultelectronUpdaterCompatibilityis now>=2.16— readfiles[]instead; pin a legacy-inclusive range only for legacy embedded updaters - Production
dependenciesthat electron-builder provides or that your bundler inlines (electron/electron-builder/react/…) are excluded from the copiednode_modulesinstead of erroring;ALLOW_ELECTRON_BUILDER_AS_PRODUCTION_DEPENDENCYremoved andelectron-prebuilt/electron-rebuildno longer guarded — tune viaignoredProductionDependencies - Building Windows targets on macOS uses the host Wine (
toolsets.winedefaults to"system") — install Wine on macOS build machines
Manual steps — see Step 3 & Step 4 above.
Plugin and custom-target authors — see the programmatic API changes.
Full breaking-changes reference
Every change above is documented in detail — with rationale (design notes) and before/after for each — on the v27 Breaking Changes page.
Migration V27 Breaking Changes (`website/docs/migration/v27-breaking-changes.md`)
title: "v27 Breaking Changes"
v27 Breaking Changes
:::danger[Read this before upgrading to v27]
v27 moves the entire electron-builder ecosystem to native ES modules, raises the minimum runtime to Node.js 22.12.0, and hard-deletes the deprecated APIs that accumulated since v22. Every breaking change in the release is catalogued on this page.
Most projects need only a Node.js bump plus one command. Run the automated migrator and follow the step-by-step migration walkthrough →.
:::
This page is the canonical reference for what changed in v27. For the how — the ordered upgrade steps, the automated migrator, and the checklist — see the v26 → v27 migration walkthrough.
- The
build()API is unchanged; configuration keys that were renamed or restructured are rewritten automatically bymigrate-schema(see each entry below) and keep their v26 behavior, and exported types that survive keep their shape. - CJS
require()continues to work without code changes on supported Node.js versions. - Every config-level breaking change below that has a mechanical v27 equivalent is rewritten automatically by
electron-builder migrate-schema(look for the Auto ✓ marker). The config keys it cannot rewrite,squirrelWindows.customSquirrelVendorDirand a custom NSIS bundle innsis.customNsisBinary/customNsisResources, are reported with a warning. Runtime, CLI, env-var, and behavior changes require manual action — see whatmigrate-schemadoes and does not do.
:::tip[Run the automated migrator first]
electron-builder migrate-schema # apply changes in place
electron-builder migrate-schema --dry-run # preview onlyThis handles every change marked Auto ✓ below. See the walkthrough for flags and serialization caveats.
:::
:::info[Toolsets now default to the newest bundle ("latest")]
In v27 every toolsets.* property defaults to "latest" — an unset property and the literal "latest" both resolve to the newest published bundle for that toolset (previously each property defaulted to a fixed pinned version; null is no longer accepted). No config change is required, but the effective defaults moved:
wine→"system"— the host-installedwineonPATH, on macOS as well as Linux (v26 downloaded a Wine 4.0.1 bundle on macOS). Building Windows targets on macOS now requires Wine on the host (brew install --cask wine-stable). The"1.0.1"Wine 11.0 bundle still downloads when pinned explicitly, but the published bundle ships no PE builtins, so it cannot run Windows tools on its own.winCodeSign→1.3.0— Windows Kits 10.0.26100.0,osslsigncode2.11 (native arm64), and the Azure Trusted Signingdlib+ .NET 8 payload.appimage→1.1.0— static FUSE3-compatible runtime; addsunsquashfssupport.icons→1.2.3— newerwasm-vips/@resvg/resvg-wasmbundle that writes 16/32 px ICNS entries as ARGB (was1.1.0).linuxToolsMac→1.0.1(runs on macOS 15+) andsevenZip→1.0.1(correct per-arch Windows binaries).nsis(1.2.1) andfpm(2.2.1) are unchanged; the newsquirreltoolset resolves to1.1.1.
To stay on a legacy bundle, pin the toolset to "0.0.0". Because winCodeSign now defaults to 1.3.0, Azure Trusted Signing uses the faster signtool /dlib path out of the box. Full breakdown in Toolsets & environment variables.
:::
Breaking changes at a glance
:::tip[Already ran migrate-schema?]
Rows marked Auto ✓ are rewritten for you. For the shortlist of changes the migrator can not apply — the new defaults and runtime behaviors you may still trip over — see What's New in v27 → new defaults & behavior changes.
:::
| Change | Auto | Action required |
|---|---|---|
| Node.js >=22.12.0 required | — | Update your runtime and CI Node version |
| All packages are native ESM | — | None — require() still works on Node >=22.12 |
electron-forge-maker-* are now ESM |
— | None — same API, same export shape |
electronCompile removed |
✓ | Remove from config; migrate off electron-compile |
disableDefaultIgnoredFiles removed |
✓ | Removed automatically; re-include specific files via a files glob (e.g. **/*.obj) |
framework / nodeVersion / launchUiVersion removed |
✓ | Removed automatically (Electron is the only framework) |
| ProtonFramework & LibUiFramework removed | — | Migrate to an Electron-based build setup |
appImage.systemIntegration removed |
✓ | Removed automatically |
npmSkipBuildFromSource removed |
✓ | Replaced by nativeModules.buildDependenciesFromSource |
Native-module options grouped under nativeModules |
✓ | nativeRebuilder → rebuildMode; the "legacy" rebuilder is removed (dropped with a warning) |
ASAR options consolidated under asar |
✓ | asarUnpack → asar.unpack, etc., including platform-level mac.asarUnpack → mac.asar.unpack (asar: true is still valid, just redundant) |
asar.ordering is now applied |
— | v26 ignored the option; v27 packs the listed files first and fails when the file is missing. Remove the option to keep the dependencies-first layout |
macOS signing consolidated under mac.sign |
✓ | identity/entitlements/hardenedRuntime/… → mac.sign.*; signIgnore → sign.ignore |
mac.universal options consolidated |
✓ | mergeASARs/singleArchFiles/x64ArchFiles → mac.universal.* |
Windows signing unified under win.sign |
✓ | Discriminated union type: "signtool" | "hsm" | "pkcs11" | "azure" |
win.signExecutable / win.signAndEditExecutable removed |
✓ | false → win.sign: false; resource editing always runs |
electronDownload → electronGet |
✓ | Reshaped to @electron/get options; some legacy fields dropped (warned) |
snap config key removed |
✓ | Restructured to snapcraft with an explicit base |
Root-level directories removed |
✓ | Move under build.directories |
build.helper-bundle-id removed |
✓ | Moved to mac.helperBundleId |
squirrelWindows.noMsi removed |
✓ | Replaced by msi (inverted) |
squirrelWindows.customSquirrelVendorDir removed |
— | Supply a custom Squirrel bundle via toolsets.squirrel (a ToolsetCustom object); migrate-schema warns but cannot rewrite it (different bundle layout) |
.customNsisBinary / .customNsisResources removed |
partial | Originally avail on nsis, nsisWeb, portable. Now combined and moved to a NSIS Toolset Bundle configured under toolsets.nsis. Manual action required - A complete migration guide is available. A config that still sets removed key(s) fails the build with a message naming the replacement. (migrate-schema warns, cannot convert it). |
GithubOptions.vPrefixedTagName removed |
✓ | Use tagNamePrefix; an empty tagNamePrefix: "" is now honored (v26 ignored it) |
GitlabOptions.vPrefixedTagName retained |
— | None — still functional; the migrator leaves GitLab entries untouched |
devMetadata / extraMetadata in PackagerOptions removed |
— | Use config / config.extraMetadata |
Implicit --publish removed |
— | Pass --publish explicitly |
--em.build / --em.directories CLI flags removed |
— | Use -c / -c.directories |
linux.syncDesktopName removed |
✓ | Removed automatically; behaviour is always on. If it was false, set desktopName to control the filename |
| Linux maintainer-script EJS syntax removed | — | Use ${var} instead of <%= var %> |
| NSIS file-association ProgID format changed | — | Update custom NSIS scripts that hard-code the old ProgID |
NSIS customInstallMode macro guard casing corrected |
— | Rename a customInstallmode (lowercase m) macro definition in custom NSIS scripts to the documented customInstallMode |
Toolset defaults resolve to "latest" |
— | No action; pin to "0.0.0" to restore a legacy bundle. Wine is now the host install — building Windows targets on macOS needs Wine installed |
toolsets.*: null rejected; appimage: "1.0.2" retired |
✓ | null entries are removed (unset = "latest"); "1.0.2" becomes "1.0.3" |
| Toolset env-var overrides removed | — | Replace APPIMAGE_TOOLS_PATH, ELECTRON_BUILDER_NSIS_DIR, USE_SYSTEM_WINE, … with toolsets.X: { url, checksum } — setting a removed variable now fails the build |
CI_BUILD_TAG env var removed |
— | Use CI_COMMIT_TAG |
Azure Trusted Signing /dlib is the default |
— | Pin winCodeSign below 1.3.0 only to force the legacy PowerShell path |
New: win.target: "msix" (beta) |
— | Optional, additive — the default winCodeSign works (only the legacy 0.0.0 bundle is rejected) |
PlatformPackager.info & platformSpecificBuildOptions now protected |
— | Plugin authors: hard break — .info no longer compiles externally; use the new pass-through getters |
Linux .desktop Exec now runs a generated *-launcher script |
— | Update custom .desktop/AppArmor/MIME tooling that hard-codes the Exec command |
node_modules arch/os-filtered on every build |
— | Awareness — packages whose cpu/os mismatch the target are now excluded |
arch: "all" now expands to x64 + arm64; 32-bit fails fast on Electron 44+ |
— | arch: "all" drops ia32 — request ia32 explicitly; ia32/armv7l require electronVersion <= 43.x |
macOS productName/executableName validated, not silently sanitized |
— | A name needing filename sanitization now throws — pick a name that needs none |
| macOS default entitlements tightened | — | The bundled default now grants only allow-jit; add allow-unsigned-executable-memory / disable-library-validation back in build/entitlements.mac.plist (and .inherit.plist) only if your app needs them |
| Bitbucket Cloud publishing: token without username → Bearer auth | — | Set BITBUCKET_USERNAME if your token is an app password / API token |
Redundant production dependencies excluded, not rejected |
— | electron/electron-builder are excluded from the copied node_modules (was a hard error); tune the set via ignoredProductionDependencies. If you set ALLOW_ELECTRON_BUILDER_AS_PRODUCTION_DEPENDENCY (removed) to bundle electron-builder, override the list instead; electron-prebuilt/electron-rebuild no longer error and now ship if declared — remove them from dependencies |
allowMissingDependencies now fails the build |
— | A missing production dependency is a hard error; set allowMissingDependencies: true to restore v26 warn-only behavior |
| Native binaries are verified against the target | — | A .node addon built for another platform/arch now fails the build; set nativeModules.verifyNativeBinaries: "warn" (or false) to package anyway |
extraFiles / extraResources to is validated |
— | An absolute to, or one escaping the output dir, now throws |
Windows publisherName validated against the certificate |
— | Update publisherName after a certificate rotation, or drop it. Custom win.sign.sign hooks (even with a certificate configured) and certificates without a CN now require it when app-update.yml is written (or set win.verifyUpdateCodeSignature: false if your updates are not Authenticode-signed) |
Custom Windows signing hook moved to win.sign.sign |
✓ | win.signtoolOptions.sign moves with the rest of signtoolOptions to win.sign.sign |
| macOS names are no longer NFD-normalized | — | Update tooling that matches the normalized on-disk bundle filename |
Linux executableArgs field codes are now literal |
— | Remove %F/%U from executableArgs; use linux.desktop.entry.Exec |
http-parser dropped from default pacman depends |
— | Add it back with "depends": ["default", "http-parser"] only if your app really needs it |
| New: Cloudflare R2 publish provider | — | None — additive; needs accountId and an https publicUrl |
New command: migrate-schema |
— | None — run it to apply every Auto ✓ change above |
DMG filesystem defaults to APFS |
— | Set dmg.filesystem: "HFS+" only if you need pre-10.13 macOS compatibility |
disableWebInstaller defaults to true (electron-updater) |
— | Web-installer updates are rejected (ERR_UPDATER_WEB_INSTALLER_DISABLED) unless disableWebInstaller is false; v27+ nsis-web installs opt in automatically; older installs, and apps switching from nsis to nsis-web, need disableWebInstaller: false |
nsis-web installer verifies and installs its own copy of the app package |
— | A --package-file package or a publish-URL download that does not match, or a --package-file package that cannot be copied, aborts the install (exit code 2); the local package file is now left in place. Only if you run one web installer with a --package-file from another build: set nsisWeb.allowUnverifiedAppPackage: true |
NSIS: per-machine builds set isAdminRightsRequired in the update info |
— | None for most apps. Their updates are started with elevate.exe; with autoInstallEvent: "onNextLaunch", per-machine apps call installPendingUpdateIfAvailable() to install a pending update |
| Update credentials are only sent to the feed's origin (electron-updater) | — | Only if latest*.yml points downloads at another origin (another host, or another port or scheme) that needs your requestHeaders / addAuthHeader credentials or a token in the feed URL's query: serve the files from the feed origin or use pre-signed URLs. A custom provider used with credential headers must override Provider.feedBaseUrl (else ERR_UPDATER_FEED_BASE_URL_NOT_DECLARED) |
| Suffixed channels expand to lower channels | — | Only with generateUpdatesFilesForAllChannels: a beta-*/latest-* channel now writes 2–3 yml files instead of 1 |
| Signed update manifests are required | — | Generate a key with electron-builder create-update-key and set ELECTRON_BUILDER_UPDATE_SIGN_KEY, or set updateManifest: false to keep publishing unsigned manifests |
app-update.yml feed follows the update-writing targets |
— | Only with target-level publish (e.g. nsis.publish), targets of one app with different feeds, or a first provider with publishAutoUpdate: false: check which feed installs will poll |
latest*.yml drops legacy top-level path/sha512 |
— | None for electron-updater >=2.16 (all modern clients); set electronUpdaterCompatibility to a legacy-inclusive range only if you still ship apps embedding electron-updater 1.x–2.15 |
quitAndInstall takes an options object (electron-updater) |
— | Replace positional args: quitAndInstall(true, false) → quitAndInstall({ isSilent: true, isForceRunAfter: false }) |
autoInstallOnAppQuit replaced by autoInstallEvent enum (electron-updater) |
— | autoInstallOnAppQuit = false → autoInstallEvent = "manual"; default "onQuit" preserves behavior |
downloadUpdate() resolves with an object, not an array (electron-updater) |
— | const [installer] = await downloadUpdate() → const { updateFile } = await downloadUpdate(); same for checkForUpdates() → downloadPromise |
verifyUpdateCodeSignature renamed and returns a result object (electron-updater) |
— | NsisUpdater.verifyUpdateCodeSignature → verifyUpdateFileAuthenticodeSignature; return { response: "success" } / { response: "failure", message } instead of null / string |
Renamed type exports (ElectronDownloadOptions, WindowsAzureSigningConfiguration, …) |
— | Import the new names — no compat aliases |
SnapOptions, ProtonFramework, LibUiFramework exports removed |
— | Use the snapcraft config shape / Electron framework |
Runtime & ESM {#runtime-esm}
Node.js >=22.12.0 required
v27 requires Node.js 22.12.0 or later. This is the version where Node's require(esm) support was stabilized (no flags needed), which allows both CJS and ESM consumers to use these packages without any code changes.
{
"engines": {
"node": ">=22.12.0"
}
}See the walkthrough for updating your runtime, CI, and Docker images.
Native ESM output
electron-builder packages now ship as native ES modules. On Node >=22.12.0 both styles continue to work:
// CJS require() — still works
const { build, Platform } = require("electron-builder")
// ESM import — now the preferred style
import { build, Platform } from "electron-builder"If your project uses "type": "module" or ESM imports, no changes are needed. If it uses CJS on Node >=22.12, require() continues to work as before. The build() function signature, all configuration options, and all exported types are otherwise unchanged.
moduleResolution — v27 packages include exports maps and full TypeScript declarations. "node" (legacy), "node16"/"nodenext", and "bundler" (recommended) all work.
`electron-forge-maker-*` plugins {#electron-forge-maker-plugins}
The four Forge maker plugins (electron-forge-maker-appimage, electron-forge-maker-nsis, electron-forge-maker-nsis-web, electron-forge-maker-snap) are now native ES modules. The public API is identical — Electron Forge loads them via dynamic import() internally, so no config changes are required.
Removed configuration options
`electronCompile`
The electronCompile configuration option has been removed. electron-compile is an unmaintained library with no releases since 2019.
{ "build": { "electronCompile": true } } // ← delete this lineIf your project relies on electron-compile for source compilation, migrate to a modern build tool before upgrading:
- electron-vite — recommended; fast, first-class ESM and Electron integration
- esbuild — extremely fast, minimal configuration
- webpack — mature, widely used
`framework` / `nodeVersion` / `launchUiVersion` {#framework-nodeversion-launchuiversion}
These three fields are removed. Only Electron is supported as a target framework — proton/proton-native and libui support has been removed (see Removed exports).
{
"build": {
"framework": "electron", // ← delete; "electron" was and remains the default
"nodeVersion": "current", // ← delete; no effect
"launchUiVersion": "0.1.0" // ← delete; no effect
}
}`disableDefaultIgnoredFiles`
The disableDefaultIgnoredFiles option has been removed. To include a file that is excluded by default (for example a Wavefront .obj 3D model, or any other default-excluded extension/name), add an explicit files glob that targets it — an explicit include now overrides the matching default exclusion:
{
"build": {
"disableDefaultIgnoredFiles": true, // ← delete this line
"files": [
"**/*",
"**/*.obj" // ← add a glob for the default-excluded files you want to keep
]
}
}Broad patterns such as **/* still honor the defaults; only a pattern that names the extension or directory concretely opts it back in.
`appImage.systemIntegration`
Removed. Desktop integration is handled automatically by AppImageLauncher.
`npmSkipBuildFromSource`
Removed. Use buildDependenciesFromSource (now under nativeModules). It is the logical inverse:
// v26
{ "npmSkipBuildFromSource": true }
// v27 equivalent
{ "nativeModules": { "buildDependenciesFromSource": false } }`asar: true` (still valid — no action needed)
asar: true remains valid in v27. The type is AsarOptions | boolean | null, the schema accepts a boolean, and the runtime maps true to {} — so an existing asar: true keeps working exactly as before. It is simply redundant, because ASAR is enabled by default:
{ "build": { "asar": true } } // Still works — but redundant
{ "build": { "asar": {} } } // Equivalent; or omit the key entirelymigrate-schema drops the redundant root-level true (folding any other ASAR keys into an asar object instead). The full ASAR restructuring is documented under ASAR options → asar.
Root-level `directories` in `package.json`
Specifying directories at the root of package.json is removed. Move it under the build key.
{ "name": "my-app", "directories": { "output": "dist" } } // Before
{ "name": "my-app", "build": { "directories": { "output": "dist" } } } // After`build.helper-bundle-id`
The hyphenated root-level helper-bundle-id is removed. Use mac.helperBundleId.
{ "build": { "helper-bundle-id": "com.example.helper" } } // Before
{ "build": { "mac": { "helperBundleId": "com.example.helper" } } } // After`squirrelWindows.noMsi`
The noMsi boolean is removed in favor of its inverse, msi.
{ "build": { "squirrelWindows": { "noMsi": true } } } // Before
{ "build": { "squirrelWindows": { "msi": false } } } // After`squirrelWindows.customSquirrelVendorDir`
Removed. v27 inlines the Squirrel.Windows installer logic and drops the electron-winstaller npm dependency (and its vendored binaries); the Squirrel vendor toolset is now fetched from the maintained squirrel.windows electron-builder-binaries bundle. To pin a version or supply a custom/local bundle — for example an air-gapped mirror — use the standard toolsets.squirrel setting instead, the same way you would pin toolsets.nsis or toolsets.winCodeSign.
The shape and behaviour differ, so this is not a 1-to-1 rename:
- The old
customSquirrelVendorDirpointed at a directory whose contents were the vendor files (Squirrel.exe,nuget.exe, …) and was copied verbatim, bypassing all provisioning. toolsets.squirrelaccepts a version pin ("1.1.1"/"latest") or aToolsetCustomobject whoseurlpoints at a bundle that contains anelectron-winstaller/vendor/subtree. The bundle still goes through normal provisioning: on every platformrcedit.exeis supplied from thewinCodeSigntoolset (it runs under Wine on non-Windows hosts), and formsi: truethe shared WiX toolset is merged in.
// Before (removed):
{ "squirrelWindows": { "customSquirrelVendorDir": "./my-vendor" } }
// After: a custom toolset bundle whose electron-winstaller/vendor/ holds the binaries.
// A bare file:// directory is used as-is (no checksum); a remote/archive url needs a checksum.
{ "toolsets": { "squirrel": { "url": "file:///abs/path/to/squirrel-toolset" } } }migrate-schema cannot rewrite this key (the bundle layout differs), so it warns and leaves it in place; the next build then fails with a message pointing at toolsets.squirrel. Remove the key to use the default bundle, or point toolsets.squirrel at a bundle with the layout above.
For a fully offline build, seed the squirrel.windows@<version> archive (plus winCodeSign and, for MSI, the WiX bundle) in the toolset cache or point toolsets.squirrel at a local bundle — see Offline / Air-Gapped Builds. Nothing is downloaded outside the checksummed toolset bundles.
customNsisBinary and customNsisResources replaced by `toolsets.nsis`
Removed (.customNsisBinary / customNsisResources), on nsis, nsisWeb and portable configs, together with the exported CustomNsisBinary type.
The 1 to 1 replacement for either/both is the new toolsets.nsis bundle. A config that still sets either key fails the build with a message naming the replacement.
In general, the NSIS resources/binary are married quite closely and in nuanced ways - the new unified structure gives easy access to both in the same place and simplifies build-flow.
See Custom NSIS Migration Guide & NSIS toolset documentation for more details.
- A Custom NSIS Binary Custom compiled NSIS binaries are practical to facillitate builds with unique requirements (like bypassing the 2Gib limit on installer.exe generation). A custom NSIS binary can also be used to adjust to the appearance of the installer in a variety of ways through the many images that get baked into it. If you were using (
.customNsisBinary) inv26, now you must provide that binary via atoolsets.nsisbundle for parity onv27. - Custom NSIS Resources Custom NSIS resources are required to expose any additional 3rd party plugins that may be needed in custom
.nshscripts. If you were using (.customNsisResources) inv26, now you must provide that resource via atoolsets.nsisbundle for parity onv27. customNsisBinary.debugLoggingmoves tonsis.installerDebugLogging(ornsisWeb.installerDebugLogging). It never had an effect onportable, which has no equivalent.
// Before (v26):
// Custom NSIS artifacts provided via 2 seperate bundles
{
"nsis": {
"customNsisBinary": {
"url": "https://example.com/my-nsis-executable.7z",
"checksum": "<checksum>",
"version": "3.10",
"debugLogging": true
},
"customNsisResources": {
"url": "https://example.com/my-nsis-resources.7z",
"checksum": "<checksum>",
"version": "3.4.1"
}
}
}
// After:
// Custom NSIS artifacts provided by a unified toolset
// Note: Debug-logging still controlled on NSIS and not by toolsets.nsis
{
"toolsets": {
"nsis": {
"url": "https://example.com/my-nsis.7z",
"checksum": "<checksum>",
"version": "3.10"
}
},
"nsis": { "installerDebugLogging": true }
}You can see the relative escape hatch of providing an archive for the full toolset is generally the same from v26 -> v27 simply in a more consolidated form.
Of note, v27 also accepts a file:// path in the url field. A file:// path to an unpacked bundle directory needs no checksum and is used as-is with no caching; a file:// path to an archive still needs a checksum.
https://— downloaded and cached locally; the archive is extracted before use.file:///absolute/path— used directly; relative paths must be within the project resources dir.
installerDebugLogging needs a log-enabled NSIS, that is makensis and its stubs compiled with NSIS_CONFIG_LOG=yes; any other makensis rejects the LogSet it emits. The bundled toolsets.nsis versions are not log-enabled, so the build fails with a configuration error when installerDebugLogging is set without a custom toolsets.nsis bundle. This is unchanged from v26, where the option also only worked with your own customNsisBinary. When building on Linux or macOS, the bundle's makensis for that host must be log-enabled too.
migrate-schema moves debugLogging to installerDebugLogging (dropping it, with a warning, for portable) and removes a customNsisBinary that sets nothing else. It cannot convert a custom bundle, so it leaves a customNsisBinary with a url / checksum / version, and any customNsisResources, in place and warns; the build then fails with the message above until you move them.
`GithubOptions` / `GitlabOptions` `vPrefixedTagName` {#githuboptions-gitlaboptions-vprefixedtagname}
The vPrefixedTagName boolean on GithubOptions is removed. Use tagNamePrefix to control the tag prefix.
{ "publish": { "provider": "github", "vPrefixedTagName": false } } // Before
{ "publish": { "provider": "github", "tagNamePrefix": "" } } // After (empty string = no prefix)To keep the default v prefix, simply remove vPrefixedTagName — tagNamePrefix defaults to "v".
An empty tagNamePrefix is now honored. v26 ignored tagNamePrefix: "" and still tagged v1.2.3 (unless vPrefixedTagName: false); v27 uses it as-is and tags 1.2.3. migrate-schema rewrites a GitHub tagNamePrefix: "" without vPrefixedTagName: false to "v" — keeping your existing tag names — and warns so you can set it back if unprefixed tags are what you wanted. A non-empty tagNamePrefix always won in v26 and is kept. The migrator applies to GitHub publish entries anywhere in the config (root, mac/win/linux, and target sections such as nsis.publish).
:::note[GitlabOptions.vPrefixedTagName is not removed]
Only the GitHub field was removed. On GitLab, vPrefixedTagName is unchanged in v27 — it still exists in the type, the schema, and the runtime, and continues to control the tag prefix (vPrefixedTagName: false → 1.2.3; omit it → v1.2.3). It has no tagNamePrefix equivalent, so migrate-schema leaves GitLab publish entries untouched. No action is required.
:::
`linux.syncDesktopName` (always synced)
The linux.syncDesktopName flag is removed. The behaviour it gated is now always on: the installed .desktop filename is always derived from the desktopName field in package.json — installed as ${desktopName}.desktop, with any trailing .desktop in the value stripped first — falling back to executableName only when desktopName is absent.
// Before (v26) — opt-in
{ "build": { "linux": { "syncDesktopName": true } } } // ← delete this linemigrate-schema removes the flag automatically. No replacement is needed. If you had syncDesktopName: false (opting out of syncing in v26), the migrator warns you: the installed filename is now always derived from desktopName, so set (or omit) desktopName to control it.
Why this changed: Electron derives its app_id / WM_CLASS from desktopName, and desktop environments match a running window to its launcher entry by comparing WM_CLASS against the installed .desktop filename. When the two diverged — which is exactly what happened with syncDesktopName: false (the v26 default) whenever a desktopName was set — GNOME, KDE, and others failed to associate the window with its launcher, breaking taskbar grouping, dock icons, and launcher highlighting (see #9103). Removing the flag eliminates a footgun rather than removing functionality. Path-traversal/NUL validation on the resulting filename is unchanged.
Linux maintainer-script EJS template syntax
The legacy <%= varName %> EJS interpolation in Linux maintainer scripts (e.g. FPM after-install/after-remove) is removed. Use shell-style ${varName} instead.
`devMetadata` / `extraMetadata` (programmatic `PackagerOptions`) {#devmetadata-extrametadata-programmatic-packageroptions}
These fields in the programmatic PackagerOptions API are removed (they have thrown InvalidConfigurationError since v22).
// Before
await build({ targets: Platform.MAC.createTarget(), devMetadata: { … }, extraMetadata: { … } })
// After
await build({ targets: Platform.MAC.createTarget(), config: { extraMetadata: { … } } })Restructured configuration
Native-module options → `nativeModules` {#native-module-options-nativemodules}
Four root-level configuration properties are moved into a new nativeModules sub-key, and nativeRebuilder is renamed to rebuildMode.
// Before (v26)
{ "build": { "buildDependenciesFromSource": true, "nodeGypRebuild": false, "npmRebuild": true, "nativeRebuilder": "parallel" } }
// After (v27)
{ "build": { "nativeModules": { "buildDependenciesFromSource": true, "nodeGypRebuild": false, "npmRebuild": true, "rebuildMode": "parallel" } } }npmArgs is not affected — it controls the package-manager install phase and remains at the root level. npmSkipBuildFromSource (deprecated in v26) is removed; migrate-schema converts it to its inverse, nativeModules.buildDependenciesFromSource.
The "legacy" value of nativeRebuilder (the old app-builder binary rebuilder) is removed — rebuildMode only accepts "sequential" or "parallel", and native modules are always rebuilt with @electron/rebuild. migrate-schema drops a "legacy" value with a warning, so the default "sequential" mode applies.
ASAR options → `asar` {#asar-options-asar}
All ASAR-related configuration is now nested under a single asar key. Flat root-level properties are removed. The asar type is now AsarOptions | false | null.
| Removed key | Replacement |
|---|---|
asar-unpack |
asar.unpack |
asar-unpack-dir |
asar.unpack |
asar.unpackDir |
asar.unpack |
asarUnpack |
asar.unpack |
disableSanityCheckAsar |
asar.disableSanityCheck |
disableAsarIntegrity |
asar.disableIntegrity |
asar: true |
(still valid — redundant, since absence means enabled) |
mac.asarUnpack / win.asarUnpack / linux.asarUnpack / mas.asarUnpack / masDev.asarUnpack |
.asar.unpack |
// Before
{ "build": { "asarUnpack": ["**/*.node"], "disableSanityCheckAsar": true, "disableAsarIntegrity": true } }
// After
{
"build": {
"asar": {
"unpack": ["**/*.node"],
"disableSanityCheck": true,
"disableIntegrity": true
}
}
}When asar: false, the root-level sub-options have no effect and the migrator removes them.
Platform-level asar now replaces the root one. In v26 the root asarUnpack, disableSanityCheckAsar, and disableAsarIntegrity applied to every platform, and a platform's own asarUnpack was added to the root patterns. In v27 all three live inside asar, and a platform-level asar (e.g. mac.asar) replaces the root asar object entirely. migrate-schema keeps the v26 result: a platform asarUnpack becomes .asar.unpack merged with the root patterns and options, and a platform that already overrides asar gets the former root-level values copied in. (For programmatic JS/TS configs, the codemod only rewrites the cases where a plain move is equivalent and warns about the rest.)
// Before (v26)
{ "asarUnpack": ["**/*.node"], "mac": { "asarUnpack": ["Resources/**"] } }
// After (v27)
{ "asar": { "unpack": ["**/*.node"] }, "mac": { "asar": { "unpack": ["**/*.node", "Resources/**"] } } }`asar.ordering` is now applied {#asar-ordering-is-now-applied}
asar.ordering was documented and validated in v26 but never read: the archive was always packed dependencies first, app files last. v27 honors it: directories keep their positions, the files listed in the ordering file are packed first in the order of the file, and everything else follows in its original order.
Two consequences for projects that already set the option:
- The archive layout changes once. Differential updates download most of
app.asaron the first update after upgrading; later updates are unaffected. - A missing ordering file is now an error. The path is resolved relative to the build resources directory, then the project directory, and the build fails with
InvalidConfigurationErrorwhen neither has it.
Remove the option to keep the default layout, which is tuned for small differential updates.
macOS signing → `mac.sign` {#macos-signing-macsign}
All macOS code-signing options now live inside a single sign object on mac (and mas / masDev), mirroring the Windows win.sign grouping. mac.sign is typed as CustomMacSign | ElectronSignOptions | string | null, where ElectronSignOptions is a typed pass-through to @electron/osx-sign.
Removed (mac.*) |
Replacement (mac.sign.*) |
|---|---|
identity |
sign.identity |
entitlements |
sign.entitlements |
entitlementsInherit |
sign.entitlementsInherit |
entitlementsLoginHelper |
sign.entitlementsLoginHelper |
provisioningProfile |
sign.provisioningProfile |
type |
sign.type (defaults to a value derived from the target: mas-dev builds default to development, everything else to distribution; an explicit value wins) |
binaries |
sign.binaries |
requirements |
sign.requirements |
hardenedRuntime |
sign.hardenedRuntime |
gatekeeperAssess |
(removed — @electron/osx-sign 2.x dropped the spctl --assess step; there is no sign.gatekeeperAssess) |
strictVerify |
sign.strictVerify |
preAutoEntitlements |
sign.preAutoEntitlements |
timestamp |
sign.timestamp |
additionalArguments |
sign.additionalArguments |
signIgnore |
sign.ignore (renamed to the @electron/osx-sign canonical name) |
// Before
{ "mac": { "identity": "Developer ID Application: My Company (TEAMID)", "hardenedRuntime": true, "signIgnore": ["**/*.txt"] } }
// After
{ "mac": { "sign": { "identity": "Developer ID Application: My Company (TEAMID)", "hardenedRuntime": true, "ignore": ["**/*.txt"] } } }To skip signing, use mac.sign.identity: null (or mac.sign: null). The same structure applies to mas and masDev.
A few of these accepted null in v26 as "unset" but do not in v27 (type, provisioningProfile, binaries, signIgnore, singleArchFiles, x64ArchFiles); migrate-schema drops those nulls instead of moving them. identity: null is kept — it still means "skip signing".
:::warning[Custom signing functions]
If you used mac.sign as a custom signing function or module path (sign: "./customSign.js") together with sibling fields like identity, the two can no longer coexist — sign is now a single union. migrate-schema leaves your custom signer untouched and prints a warning so you can decide whether to keep the custom function or switch to an ElectronSignOptions object.
:::
macOS default entitlements tightened {#macos-default-entitlements-tightened}
The bundled default entitlements file (used when neither mac.sign.entitlements nor build/entitlements.mac.plist is present) now grants only com.apple.security.cs.allow-jit. com.apple.security.cs.allow-unsigned-executable-memory and com.apple.security.cs.disable-library-validation are no longer granted by default — modern Electron does not need either in the main process, and both materially weaken the Hardened Runtime.
- Nested binaries (helpers, frameworks,
.nodemodules, executables inapp.asar.unpacked) are no longer signed with one blanket plist. They now receive@electron/osx-sign's per-file defaults — renderer/GPU helpers:allow-jit; plugin helper: Chromium's looser set; everything else:default.darwin.plist— unless you supplybuild/entitlements.mac.inherit.plist/mac.sign.entitlementsInherit. - MAS builds without a
build/entitlements.mas.plistfall back to@electron/osx-sign's sandboxeddefault.mas.plist(previously they received the non-sandboxedmactemplate). - Ad-hoc builds (
mac.sign.identity: "-") keep working: a built-in ad-hoc entitlements file that includesdisable-library-validationis applied to the app and its nested binaries. - New post-sign warning: after signing, electron-builder lists Mach-O libraries and bundles under
Contents/Resources/app.asar.unpackedandContents/PlugIns(which electron-builder never re-signs) whose signature carries a different Team ID (or none) whiledisable-library-validationis not granted — these would fail library validation when loaded. Executables are spawned rather than loaded, so they are not subject to library validation and are not flagged.
Action is required only if your app loads frameworks, plugins, or native modules signed by another Team ID (or unsigned), or still relies on allow-unsigned-executable-memory: add the keys back in build/entitlements.mac.plist (and in build/entitlements.mac.inherit.plist when a helper process or a sidecar executable does the loading — library validation is enforced by the loading process based on its own entitlements, not by what the nested file was signed with). See Loading third-party or unsigned binaries.
This is a runtime default, not a config-key rename, so migrate-schema does not change it.
`mac.universal`
The universal-build options move from mac root into a universal object (typed as ElectronUniversalOptions, a pass-through to @electron/universal). They have no effect unless the target arch is universal.
Removed (mac.*) |
Replacement (mac.universal.*) |
|---|---|
mergeASARs |
universal.mergeASARs |
singleArchFiles |
universal.singleArchFiles |
x64ArchFiles |
universal.x64ArchFiles |
{ "mac": { "mergeASARs": true, "singleArchFiles": "*.node" } } // Before
{ "mac": { "universal": { "mergeASARs": true, "singleArchFiles": "*.node" } } } // AfterWindows signing → `win.sign` {#windows-signing-winsign}
In v26, Windows signing used two separate root-level keys (signtoolOptions and azureSignOptions). In v27, all Windows signing modes are expressed through a single win.sign key typed as a discriminated union:
win.sign: { type: "signtool" | "hsm" | "pkcs11" | "azure", … } | false | nullfalse / null disables signing entirely. Unset means electron-builder discovers credentials from the environment (e.g. WIN_CSC_LINK).
win.signtoolOptions → win.sign: { type: "signtool", … } — all fields move verbatim; add type: "signtool".
{ "win": { "signtoolOptions": { "certificateFile": "cert.pfx", "publisherName": "CN=ACME Inc" } } } // Before
{ "win": { "sign": { "type": "signtool", "certificateFile": "cert.pfx", "publisherName": "CN=ACME Inc" } } } // Afterwin.azureSignOptions → win.sign: { type: "azure", … } — fields move into win.sign with type: "azure". The old index signature ([k: string]: string) that allowed arbitrary extra keys is replaced by an explicit additionalMetadata object. migrate-schema both restructures the key and moves any unrecognized extra keys into additionalMetadata.
// Before
{ "win": { "azureSignOptions": { "endpoint": "https://weu.codesigning.azure.net/", "certificateProfileName": "my-profile", "ExcludeCredentials": "ManagedIdentityCredential" } } }
// After
{ "win": { "sign": { "type": "azure", "endpoint": "https://weu.codesigning.azure.net/", "certificateProfileName": "my-profile", "additionalMetadata": { "ExcludeCredentials": "ManagedIdentityCredential" } } } }New signing modes (beta): `hsm` and `pkcs11`
v27 introduces two new signing modes in win.sign:
type: "hsm"— Hardware Security Module via signtool/csp /kc(Windows-only; requirestoolsets.winCodeSign: "1.x").type: "pkcs11"— PKCS#11 token via osslsigncode (cross-platform; runs on macOS/Linux CI without a Windows VM).
{
"win": {
"sign": {
"type": "pkcs11",
"pkcs11Module": "/usr/lib/x86_64-linux-gnu/opensc-pkcs11.so",
"pkcs11KeyUri": "pkcs11:token=MyToken;object=MyKey;type=private",
"certificateFile": "cert.pem"
}
},
"toolsets": { "winCodeSign": "1.3.0" }
}Both HSM and PKCS#11 are beta — the interfaces are stable but real-hardware test coverage is limited.
Custom Windows signing hooks move to `win.sign.sign`
In v26 the custom signer hook lived at win.signtoolOptions.sign. v27 reuses the win.sign key for the discriminated union, and the hook lives inside the signtool-family config, so it moves together with the rest of signtoolOptions:
{ "win": { "signtoolOptions": { "sign": "./customSign.js" } } } // Before (v26)
{ "win": { "sign": { "type": "signtool", "sign": "./customSign.js" } } } // After (v27)migrate-schema performs this move. (A top-level win.sign: "./customSign.js" is a pre-v26 shape that v26 already rejected; move it under { "type": "signtool", "sign": … } by hand.)
A hook now also needs win.sign.publisherName, even with a certificate configured (the publisher name of a hook is never derived from a certificate) — see Windows publisherName.
`win.signExecutable` / `win.signAndEditExecutable` removed {#winsignexecutable-winsignandeditexecutable-removed}
| Removed | Replacement |
|---|---|
win.signExecutable: false |
win.sign: false (disables signing; resource editing still runs) |
win.signExecutable: true |
(deleted — signing is enabled by default when credentials are available) |
win.signAndEditExecutable: true |
(deleted — resource editing always runs) |
win.signAndEditExecutable: false |
win.sign: false for the signing half; no equivalent for skipping resource editing — see note below |
:::warningwin.signAndEditExecutable: false formerly skipped both resource editing (icon, metadata) and signing. In v27 resource editing always runs. migrate-schema maps the signing half to win.sign: false and warns about the rest. If you need to skip resource editing for a specific artifact, apply resources manually after building.
:::
Both flags set to false skipped all signing in v26, so any win.signtoolOptions / win.azureSignOptions next to them never applied. migrate-schema removes those blocks (naming them in a warning, without printing their values) rather than turning signing on; to sign in v27, replace win.sign: false with a win.sign: { type: … } object.
`electronDownload` → `electronGet` {#electrondownload-electronget}
The electronDownload configuration key is renamed to electronGet and reshaped to match @electron/get's options directly (v27 upgrades to @electron/get v5, which downloads via fetch).
Old electronDownload.* |
New electronGet.* |
|---|---|
mirror |
mirrorOptions.mirror |
isVerifyChecksum: false |
unsafelyDisableChecksums: true |
cache |
(not a config key — set the ELECTRON_BUILDER_CACHE environment variable) |
customDir, customFilename, strictSSL, force, platform, arch, version |
(no equivalent — dropped by migrate-schema with a warning) |
{ "electronDownload": { "mirror": "https://my-mirror/" } } // Before
{ "electronGet": { "mirrorOptions": { "mirror": "https://my-mirror/" } } } // After`snap` → `snapcraft` {#snap-snapcraft}
The top-level snap configuration key is removed. Use snapcraft with an explicit base field and per-base options nested under a sub-key named after the base.
// Before
{ "build": { "snap": { "confinement": "strict", "stagePackages": ["libfoo"], "base": "core22" } } }
// After
{ "build": { "snapcraft": { "base": "core22", "core22": { "confinement": "strict", "stagePackages": ["libfoo"] } } } }Supported base values: "core18", "core20", "core22", "core24", and "custom". migrate-schema performs this restructuring; when the old config has no base, it assumes