electron-builder Documentation

A complete solution to package and build a ready for distribution Electron app with “auto update” support out of the box

RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy electron-builder on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name electron-builder --restart always electron-userland/electron-builder:latest

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:
  • 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.
  • 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:

bash
electron-builder migrate-schema

Step-by-step walkthrough: v26 → v27 migration guide · electron.build/docs/migration/v26-to-v27

Installation

text
yarn add electron-builder --dev
// or npm, pnpm, bun

Note 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:

text
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.

  1. Clone the starter and add electron-builder:

    bash
    git clone https://github.com/electron/electron-quick-start
    cd electron-quick-start
    npm install
    npm install electron-builder --save-dev
  2. Specify the standard fields in the application package.json — name, description, version and author.

  3. Specify the build configuration in the package.json as 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 as js, ts, yml, and json/json5. See read-config-file for supported extensions. JS Example for programmatic API

  4. Add icons.

  5. 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)) or yarn 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 your package.json.

  6. 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.

js
// ESM (recommended)
import { build, Platform } from "electron-builder"

await build({
  targets: Platform.MAC.createTarget(),
  config: {
    // build options — see https://www.electron.build/
  }
})
js
// 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

Debug

Set the DEBUG environment variable to debug what electron-builder is doing:

bash
DEBUG=electron-builder

FPM_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.

bash
set DEBUG=electron-builder

:::

:::tip[PowerShell]
PowerShell uses different syntax to set environment variables.

bash
$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.

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_modules because NPM is unreliable and NPM team doesn't want to fix it.

    ${PWD##*/}-node-modules is 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.

http://www.mathiaswestin.net/2012/09/how-to-make-per-user-installation-with.html

https://msdn.microsoft.com/en-us/library/windows/desktop/dd378457(v=vs.85).aspx#FOLDERID_UserProgramFiles

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.

http://stackoverflow.com/questions/2565215/checking-if-the-application-is-running-in-nsis-before-uninstalling

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:

bash
chmod +x MyApp-1.0.0-x86_64.AppImage
./MyApp-1.0.0-x86_64.AppImage

Extracting an AppImage

You can extract the contents of any AppImage without running it:

bash
./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:

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 .desktop entry 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:

bash
# 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:

yaml
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-window

See Linux Configuration for the full desktop entry reference.

MIME Type Associations

Associate your AppImage with file types via linux.mimeTypes:

yaml
linux:
  mimeTypes:
    - application/x-myapp-document

Auto-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:

yaml
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:

text
dlopen(): error loading libfuse.so.2
AppImages require FUSE to run.
yaml
toolsets:
  appimage: "0.0.0"   # legacy FUSE2 runtime — only if you need it

Compression

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 explicit appImage.compression of xz or gzip is forwarded to mksquashfs; otherwise the root compression: "maximum" selects xz. All other values use mksquashfs's default (gzip).
  • Static runtime (default): supports gzip and zstd. The appImage.compression option 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):

yaml
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:

yaml
appImage:
  license: build/EULA.txt

The 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:

bash
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:

bash
DEBUG=electron-builder electron-builder --linux AppImage

Test the built AppImage:

bash
chmod +x dist/MyApp-1.0.0.AppImage
./dist/MyApp-1.0.0.AppImage --no-sandbox

The --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

yaml
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:

yaml
appx:
  capabilities:
    - runFullTrust                  # default — allows unrestricted execution
    - internetClient               # outbound internet access
    - privateNetworkClientServer   # local network
    - webcam                       # camera access
    - microphone                   # microphone access

runFullTrust is required for most Electron apps (included by default). The full list of capabilities is in Microsoft's documentation.

Tile Customization

yaml
appx:
  backgroundColor: "#2c2c2c"    # tile background color (CSS color)
  showNameOnTiles: false        # show app name on tile (default: false)

Language Support

yaml
appx:
  languages:
    - en-US
    - de-DE
    - fr-FR
    - ja-JP

Declare which languages your app supports. This affects Store listings and localized resources.

Version Targeting

yaml
appx:
  minVersion: "10.0.17763.0"       # Windows 10 1809 minimum
  maxVersionTested: "10.0.22621.0" # Windows 11 22H2 tested maximum

Auto-Launch Extension

yaml
appx:
  addAutoLaunchExtension: false    # Default: false

When 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:

yaml
appx:
  customManifestPath: build/AppxManifest.xml

Or inject additional XML extensions:

yaml
appx:
  customExtensionsPath: build/appx-extensions.xml

Publishing to the Windows Store

  1. Create a Microsoft Developer account ($19 one-time fee)
  2. Reserve your app name in Partner Center
  3. Build your AppX: electron-builder --win appx
  4. Test the package locally (see Testing below)
  5. Upload the .appx file via Partner Center submission
  6. Complete the submission (age rating, pricing, screenshots, etc.)
  7. Wait for Store certification (typically 1-3 business days)

Sideloading Without the Store

For enterprise distribution without the Store:

powershell
# 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

powershell
# 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-AppxPackage

Common 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

bash
# 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 --linux

Configuration

yaml
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

bash
electron-builder --mac --universal

Or set in configuration:

yaml
mac:
  target:
    - target: dmg
      arch: universal

electron-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:

yaml
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 build

Arch-Specific Native Modules

If you have native modules that differ between x64 and arm64, you must configure which files should remain architecture-specific:

yaml
mac:
  universal:
    mergeASARs: true
    singleArchFiles: "node_modules/canvas/**"   # only exists in x64

For modules that exist in both architectures but have different binaries:

yaml
mac:
  universal:
    mergeASARs: true
    x64ArchFiles: "node_modules/my-module/build/Release/my_module.node"

Verifying a Universal Binary

bash
# 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.).

bash
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) or arm64 (Pi 4+)
  • ARM servers — arm64 (AWS Graviton, Ampere Altra, etc.)
  • Embedded Linux — armv7l
bash
# For Raspberry Pi 4+ and ARM servers
electron-builder --linux --arm64

# For older Raspberry Pi (32-bit OS)
electron-builder --linux --armv7l

Building Linux ARM on x64

Linux ARM can be built on an x64 machine using QEMU emulation via Docker:

bash
# 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:

bash
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:

bash
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:

yaml
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:

yaml
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 }}

Cli (`website/docs/cli.md`)


title: CLI

text
/* 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).

bash
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.json build key, or electron-builder.{yml,yaml,json,json5,toml,js,cjs,mjs,ts}. Pass --config for 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 toml library 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 (an nsis-web target, a per-machine nsis / nsisWeb installer, a custom win.sign.sign hook without a publisher name, a generic publish url with 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:
    • if you build on Windows or macOS: Snap and AppImage for x64.
    • if you build on Linux: Snap and AppImage for current architecture.

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

json
"build": {
  "win": {
    "target": [
      {
        "target": "nsis",
        "arch": [
          "x64",
          "arm64"
        ]
      }
    ]
  },
  "mac": {
    "target": [
      {
        "target": "dmg",
        "arch": [
          "universal"
        ]
      }
    ]
  }
}

electron-builder.yml

yaml
win:
  target:
    - target: nsis
      arch:
        - x64
        - arm64
mac:
  target:
    - target: dmg
      arch: universal

electron-builder.config.js

js
module.exports = {
  "win": {
    "target": [
      {
        "target": "nsis",
        "arch": [
          "x64",
          "arm64"
        ]
      }
    ]
  },
  "mac": {
    "target": [
      {
        "target": "dmg",
        "arch": [
          "universal"
        ]
      }
    ]
  }
}

:::

and use

text
build -wl

TargetConfiguration

  • target String - 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.json file of your project using the build key on the top level:

    json
    "build": {
      "appId": "com.example.app"
    }
  • or through the --config option. Defaults to electron-builder.yml.

    yaml
    appId: "com.example.app"

    json, json5, toml or js/ts (exported configuration or function that produces configuration) formats are also supported.

    :::tip
    If you want to use a js file, do not name it electron-builder.js. It will conflict with the electron-builder package name.
    :::

    :::tip
    If you want to use toml, install it with npm 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:

json5
{
  "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:

  1. App files — your application source code and assets, placed inside the ASAR archive (or directly in the app folder if ASAR is disabled)
  2. 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_modules dev 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_output
  • appveyor.yml, .travis.yml, circle.yml
  • npm-debug.log, yarn.lock, .yarn-integrity, .yarn-metadata.json
  • Test directories: {test,__tests__,tests,powered-test,example,examples} inside node_modules
  • *.d.ts files inside node_modules
  • node_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:

yaml
files:
  - "**/*"
  - "!src/**"           # exclude source files
  - "!tests/**"         # exclude test directory
  - "!**/*.map"         # exclude source maps
  - "dist/**"           # include dist directory

Important 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.json and node_modules/**/* (production only) are always included regardless of patterns.
  • All default ignores (listed above) are always applied.

Simple Include Example

yaml
files:
  - "**/*"
  - "!src/**"
  - "!*.ts"
  - "!tsconfig*.json"

Only Include Specific Directories

yaml
files:
  - dist/**
  - assets/**
  - node_modules/**     # production node_modules (already automatic)
  - package.json        # always included, but can be explicit

Exclude Large Unused Files

yaml
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:

yaml
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

yaml
files:
  - from: dist/main.js
    to: app.js

Copying from Outside the App Directory

yaml
files:
  - "**/*"
  - from: ../shared/assets
    to: shared-assets

Extra 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/
yaml
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
yaml
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.

yaml
asar: {}    # ASAR is enabled by default — pass an object to configure, or set asar: false to disable

Set 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 (.node files)
  • 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/:

yaml
asar:
  unpack:
    - "**/*.node"                   # all native modules
    - "resources/ffmpeg"            # large binary
    - "**/**/node_modules/sharp/**" # specific module

Files in app.asar.unpacked/ are accessible via the same paths as if they were in the ASAR — Electron transparently redirects reads.

Smart Unpack

yaml
asar:
  smartUnpack: true    # Default: true

When 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:

yaml
asar:
  ordering: build/asar-ordering.txt
text
# build/asar-ordering.txt — list files loaded at startup first
index.js
renderer.js
node_modules/electron/index.js

Practical Examples

Electron App with Native Modules

yaml
files:
  - "**/*"
  - "!src/**"
  - "!**/*.ts"
  - "!**/*.map"
asar:
  unpack:
    - "**/*.node"

App with Large Video Assets

yaml
files:
  - "**/*"
  - "!src/**"
  - "!videos/**"      # exclude from ASAR
extraResources:
  - from: videos/
    to: videos         # place outside ASAR, accessible via process.resourcesPath

Monorepo Setup (Two-Package Structure)

yaml
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 and
package-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:

yaml
files:
  - "**/*"
  - "**/*.obj"        # keep Wavefront .obj 3D models that are excluded by default

The same applies to default-excluded names — reference the name in a pattern segment, e.g.:

yaml
files:
  - "**/*"
  - "**/.github/**"   # keep the .github directory that is excluded by default

Only 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:

yaml
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 zip target 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:

yaml
dmg:
  window:
    position:
      x: 130
      y: 220
    size:
      width: 540
      height: 380

Standard 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:

yaml
dmg:
  background: build/background.png

Requirements:

  • Format: PNG (recommended for quality and transparency support)
  • Size: match your window.size — typically 540×380 pixels
  • For Retina displays: provide a @2x version 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:

yaml
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:

yaml
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:

yaml
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: 12

The 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.
yaml
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:

yaml
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:

yaml
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

yaml
dmg:
  internetEnabled: true   # Default: false

Internet-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.

Specify the license file path directly in electron-builder.yml:

yaml
dmg:
  license: "build/license.rtf"

For multi-language builds, provide a language-code → file-path map:

yaml
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:

json
{
  "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

yaml
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:

yaml
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: UDZO

Troubleshooting

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!}



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_USERNAME set → 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.
bash
DEBUG=electron-builder electron-builder --linux snap

Namespaced 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

js
[
  // 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:

json5
{
  "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 to ia32, x64. If no arch, macro will be removed from your pattern with leading space, - and _ (so, you don't need to worry and can reuse pattern).
  • ${os} — expanded to mac, linux or win according to target platform.
  • ${platform} — expanded to darwin, linux or win32 according to Node.js process.platform property.
  • ${name} – package.json name.
  • ${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:

yaml
flatpak:
  runtime: org.freedesktop.Platform         # freedesktop (most common for Electron)
  sdk: org.freedesktop.Sdk
  runtimeVersion: "24.08"                   # runtime version

Common 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:

yaml
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"      # audio

Common 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

yaml
flatpak:
  branch: stable    # Default: master/main

The branch name is embedded in the Flatpak bundle and visible to users.

Extra Modules

Include additional system libraries not in the runtime:

yaml
flatpak:
  modules:
    - name: zlib
      buildsystem: autotools
      sources:
        - type: archive
          url: https://www.zlib.net/zlib-1.3.tar.gz
          sha256: "..."

Copy additional files into the app bundle:

yaml
flatpak:
  files:
    - - build/extra-binary        # source
      - /app/bin/extra-binary     # destination (absolute within Flatpak)
  symlinks:
    - - /app/bin/myapp            # target
      - /app/bin/myapp-alias      # symlink location

Wayland Support

yaml
flatpak:
  useWaylandFlags: true   # Appends --enable-features=UseOzonePlatform --ozone-platform=wayland when running under a Wayland session

Local Testing

Build and install the Flatpak locally:

bash
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.myapp

Troubleshooting

If the build fails, enable debug logging to see the underlying flatpak-builder output:

bash
env DEBUG="@malept/flatpak-bundler" electron-builder build --linux flatpak

Missing 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. Use GITHUB_TOKEN (provided automatically) or a PAT with repo scope

Minimal Multi-Platform Workflow

This workflow builds for macOS, Windows, and Linux on every push to main and on version tags:

yaml
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):

yaml
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:

yaml
# In electron-builder.yml
publish:
  provider: github
  releaseType: draft

Then in GitHub: go to Releases → find the draft → click "Edit" → "Publish release".

macOS Code Signing Setup

Preparing the Certificate

bash
# 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 clipboard

Add to GitHub Secrets:

  • MAC_CSC_LINK — the base64-encoded .p12 content
  • MAC_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:

yaml
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

bash
# 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.txt

Add to GitHub Secrets:

  • WIN_CSC_LINK — base64-encoded .pfx content
  • WIN_CSC_KEY_PASSWORD — certificate password

Using Azure Trusted Signing

yaml
- 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:

yaml
- 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 -):

yaml
- 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:

text
/* 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.


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:

yaml
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:

yaml
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-file

Standard 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:

json title="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:

yaml
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.

yaml
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: optional

Common depends packages for Electron apps:

  • libgtk-3-0 — GTK3 (required)
  • libnotify4 — desktop notifications
  • libnss3 — NSS (Chromium networking)
  • libxss1 — screen saver extension
  • libxtst6 — X test extension
  • xdg-utils — desktop integration utilities
  • libatspi2.0-0 — accessibility
  • libuuid1 — UUID support
  • libsecret-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.

yaml
rpm:
  depends:
    - libXScrnSaver
    - libnotify
    - libappindicator
    - nss
    - gtk3
  compression: xz     # gz | bzip2 | xz | lzo (default: xz)
  packageCategory: Applications/Internet

Common depends for Electron on RPM systems:

  • gtk3 — GTK3
  • libnotify — notifications
  • libXScrnSaver — screen saver
  • nss — networking

After-install/after-remove scripts:

yaml
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.):

yaml
pacman:
  depends:
    - gtk3
    - libnotify
    - nss
    - libxss
    - libxtst
    - xdg-utils
    - at-spi2-core
    - libsecret

Alpine APK Package (`apk`)

Creates an .apk package for Alpine Linux, commonly used in Docker containers:

yaml
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:

yaml
freebsd:
  depends:
    - gtk3
    - libnotify
    - nss

FPM Passthrough Options

The DEB, RPM, Pacman, APK, FreeBSD, and P5P targets are built using FPM internally. Pass additional FPM command-line arguments:

yaml
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:

yaml
deb:
  appArmorProfile: build/apparmor-profile

Without 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:

yaml
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:

yaml
mac:
  target:
    - target: dmg
      arch: universal

Universal binary options:

  • mergeASARs — merge x64 and arm64 ASAR archives into a single universal ASAR (true by 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:

yaml
mac:
  universal:
    mergeASARs: true
    singleArchFiles: "**/*.node"   # keep native modules as separate arch files

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:

yaml
mac:
  sign:
    identity: "Developer ID Application: My Company (TEAM1234AB)"

Or use environment variables — the recommended approach for CI:

bash
export CSC_LINK=/path/to/certificate.p12
export CSC_KEY_PASSWORD=yourpassword

Set 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
<?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:

yaml
mac:
  sign:
    entitlements: build/entitlements.mac.plist
    entitlementsInherit: build/entitlements.mac.inherit.plist

Common 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:

yaml
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).

yaml
mac:
  sign:
    binaries:
      - Contents/MacOS/my-native-helper
      - Contents/Frameworks/MyFramework.framework/Versions/A/MyFramework

Product / 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.

yaml
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
<?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
<?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:

yaml
mas:
  sign:
    entitlements: build/entitlements.mas.plist
    entitlementsInherit: build/entitlements.mas.inherit.plist
    provisioningProfile: build/MyApp_AppStore.provisionprofile

Common 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.

yaml
masDev:
  sign:
    provisioningProfile: build/MyApp_Dev.provisionprofile
    entitlements: build/entitlements.mas.plist
    entitlementsInherit: build/entitlements.mas.inherit.plist

Build the dev target:

bash
electron-builder --mac mas-dev

Building for the App Store

bash
electron-builder --mac mas

This produces:

  • MyApp-<version>.pkg — the package to upload to App Store Connect

Submitting to App Store Connect

  1. Open App Store Connect
  2. Create your app listing (if new) under My Apps → +
  3. Use Transporter (free, from the Mac App Store) or xcrun altool / xcrun notarytool to upload the .pkg
  4. The uploaded build appears in App Store Connect after processing (usually a few minutes)
  5. 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, NSCameraUsageDescription etc. must be in Info.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:

  1. Building your normal NSIS installer
  2. Wrapping the NSIS .exe inside a minimal MSI package
  3. 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:

yaml
msiWrapped:
  wrappedInstallerArgs: "/S /D=C:\\MyApp"

Impersonation

yaml
msiWrapped:
  impersonate: false    # Default: false

When 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

yaml
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

yaml
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 /quiet and /passive flags 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.
:::

yaml
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

yaml
msi:
  oneClick: true    # Default: true

When 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

yaml
msi:
  perMachine: false   # Default: false (per-user install)
  • perMachine: false — installs for the current user only, no admin required
  • perMachine: 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:

yaml
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 completes

WiX Customization

The MSI target uses WiX Toolset internally. You can pass additional arguments to the WiX compiler (candle.exe) and linker (light.exe):

yaml
msi:
  additionalWixArgs:
    - "-dMyVariable=MyValue"
    - "-ext WixUIExtension"
  additionalLightArgs:
    - "-sice:ICE80"

Use msiProjectCreated hook to modify the generated WiX project files before they are compiled:

yaml
msiProjectCreated: ./msi-hook.js

Warnings as Errors

yaml
msi:
  warningsAsErrors: true    # Default: true

WiX 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:

cmd
# 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

yaml
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: true

Troubleshooting

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):

yaml
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

yaml
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"
powershell
electron-builder --win msix

MSIX 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

yaml
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 / Store

Defaults when omitted:

  • applicationId → derived from identityName (a leading numeric prefix such as 01234 is stripped automatically, with a warning), falling back to the application name. Validated to 1–64 characters.
  • identityName → the application name. Validated to 3–50 characters of [a-zA-Z0-9.-].
  • publisherDisplayName → company name from your application metadata (author in package.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:

yaml
msix:
  capabilities:
    - runFullTrust                  # required for Electron apps — always included
    - internetClient                # outbound internet access
    - privateNetworkClientServer    # local network
    - webcam                        # camera access
    - microphone                    # microphone access

runFullTrust 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

yaml
msix:
  backgroundColor: "#2c2c2c"    # tile background color (CSS color). Default: #464646
  showNameOnTiles: false        # overlay app name on the wide & square tiles. Default: false

Optional 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

yaml
msix:
  languages:
    - en-US     # first entry is the default language
    - de-DE
    - fr-FR
    - ja-JP

Declares 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

yaml
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 omitted

These map to MinVersion / MaxVersionTested on <TargetDeviceFamily Name="Windows.Desktop" />.

yaml
msix:
  setBuildNumber: false   # include the build number in the package version. Default: false

Package Integrity

yaml
msix:
  enforcePackageIntegrity: true   # Default: false

When 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.

yaml
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

yaml
msix:
  addAutoLaunchExtension: true

When 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:

yaml
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:

yaml
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:

yaml
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 .msixbundle is produced only when more than one architecture is built and createMsixbundle is not false.
  • A .msixupload is 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 publisher is 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

powershell
# 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-AppxPackage

To 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

  1. Create a Microsoft Developer account and reserve your app name in Partner Center.
  2. Build with createMsixupload: true to produce a .msixupload.
  3. Upload the .msixupload via the Partner Center submission flow.
  4. 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:

  1. An active Apple Developer Program membership ($99/year)
  2. A Developer ID Application certificate (not Mac App Store)
  3. Hardened Runtime enabled in your build configuration
  4. Required entitlements set (see below)
  5. Authentication credentials (Apple ID + app-specific password, or API key)

Enabling Notarization

yaml
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:

bash
[email protected]
APPLE_APP_SPECIFIC_PASSWORD=xxxx-xxxx-xxxx-xxxx
APPLE_TEAM_ID=ABCDE12345

API keys don't expire and don't require two-factor authentication, making them ideal for CI.

  1. Go to App Store Connect → Users and Access → Keys
  2. Create a new key with "Developer" role
  3. Download the .p8 file (only downloadable once)
  4. Note the Key ID and Issuer ID

Then set the following environment variables:

bash
# 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=ABCDE12345

Authentication — Option C: Keychain Profile (stored credentials)

If you have previously stored notarization credentials via xcrun notarytool store-credentials, reference them by profile name:

bash
APPLE_KEYCHAIN_PROFILE=my-notarization-profile
APPLE_KEYCHAIN=/path/to/keychain.keychain-db  # optional; defaults to system keychain

This 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
<?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

yaml
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:

bash
# 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.app

Troubleshooting

"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.


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-independent latest URL) 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.
:::

  1. 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).
  2. Define wanted macro to customise: customHeader, preInit, customInit, customUnInit, customInstall, customUnInstall, customRemoveFiles, customInstallMode, customWelcomePage, customUnWelcomePage, customUnInstallSection.

:::note[Example]

nsis
!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_DIR and PROJECT_DIR are defined.
  • build is added as addincludedir (i.e. you don't need to use BUILD_RESOURCES_DIR to !include sibling files from the build resources directory).
  • build/x86-unicode and build/x86-ansi are added as addplugindir (each one only when the directory exists, regardless of the unicode option).
  • File associations macro registerFileAssociations and unregisterFileAssociations are 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:

nsis
!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}.

nsis
${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:

nsis
!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

json
"build": {
  "nsis": {
    "oneClick": false
  }
}

electron-builder.yml

yaml
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.

yaml
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:

yaml
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)
yaml
pkg:
  scripts: build/pkg-scripts   # default

Example postinstall script:

bash
#!/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:

yaml
pkg:
  welcome: build/welcome.html    # or .rtf, .txt
  license: build/license.html    # or .rtf, .txt
  conclusion: build/conclusion.html

Supported file formats for each page: HTML, RTF, plain text.

Background image — displayed behind the installer pages (macOS 10.5+):

yaml
pkg:
  background:
    file: build/installer-background.png
    alignment: center       # center | left | right | top | bottom | topleft | topright | bottomleft | bottomright
    scaling: tofit          # tofit | proportional | none

Installation 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:

yaml
pkg:
  mustClose:
    - com.mycompany.myapp
    - com.mycompany.myapp.helper

The 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:

yaml
pkg:
  extraPkgsDir: build/extra-pkgs

All .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

yaml
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: upgrade

Troubleshooting

"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.

text
/* Detailed source-code truncated for AI context efficiency. */

CJS equivalent (Node >=22.12.0):

javascript
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_TOKEN will 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 the GITHUB_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):

json
{
  "build": {
    "win": {
      "publish": ["github", "bitbucket"]
    }
  }
}
yaml
win:
  publish:
      # an object provider for github with additional options
    - provider: github
      protocol: https
      # a string provider for bitbucket that will use default options
    - bitbucket

Which 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:

  1. 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 own publish: the target-level value (e.g. nsis.publish) first, then the platform level (win.publish), then the top level.
  2. 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: false gets no latest*.yml, so it is skipped (with publish: [{ provider: "s3", …, publishAutoUpdate: false }, "github"] the feed is GitHub). They must agree: two such targets whose first providers differ (for example nsis.publish pointing at S3 while an AppX inherits win.publish: github, or dmg.publish differing from zip.publish) would leave some installs polling a feed that never receives their manifest. A publishing build then fails at build start with an InvalidConfigurationError; a build without a publish policy logs a warning that publishing will fail, and writes no app-update.yml for that app. Configure publish once 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 as url, bucket, region, path, owner, repo or channel; options that only affect the upload (publishAutoUpdate, timeout, and for S3-compatible providers acl, storageClass, encryption) may differ.
  3. If none of them emits a manifest — only non-updating targets such as snap or portable, or a target with publish: null or publishAutoUpdate: false on every provider — the platform/top-level publish is used, as before.
  4. If no level configures publish at all, the feed falls back to GitHub when the repository field points there (see GitHub Repository).

For example, with only a target-level setting

yaml
nsis:
  publish:
    provider: s3
    bucket: my-updates

installed 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:

text
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:

json
"release": "electron-builder"

and if you run yarn release, a release will be drafted (if doesn't already exist) and artifacts published.

  1. Draft a new release. Set the "Tag version" to the value of version in your application package.json, and prefix it with v. "Release title" can be anything you want.

    For example, if your application package.json version is 1.0, your draft's "Tag version" would be v1.0.

  2. Push some commits. Every CI build will update the artifacts attached to this draft.

  3. 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).

  1. Setup your CI to publish on each commit. E.g. "dist": "electron-builder --publish always" in your package.json.
  2. Set your version in your application package.json to 1.9.0-snapshot (or 1.9.0-master or whatever you want your development channel to be named). This will publish a file named snapshot.yml and a build named something-snapshot.exe (and corresponding for mac) to S3.
  3. When you are ready to deploy, simply change your package version to 1.9.0 and push. This will then produce a latest.yml and something.exe on s3. Usually you'll git-tag this version as well (just to keep track of it).
  4. 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/config origin 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:

text
• 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-app

Check 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:

json5
{
  "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 the BITBUCKET_USERNAME env 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:

  1. Create an R2 API token with at least Object Read & Write permission on your bucket.
  2. Expose its credentials to electron-builder via the CF_R2_ACCESS_KEY_ID and CF_R2_SECRET_ACCESS_KEY environment variables.
  3. Enable public access for the bucket via an r2.dev subdomain or a custom domain — R2's S3 API endpoint cannot serve unauthenticated downloads, so electron-updater cannot download updates from it.
  4. Set publicUrl to 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.


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.

json
{
  "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).

json
{
  "snapcraft": {
    "base": "core24",
    "core24": {
      "useLXD": true
    }
  }
}

LXD must be installed and the build user must be in the lxd group:

sh
sudo snap install lxd
sudo usermod -aG lxd $USER
lxd init --minimal

Multipass

VM-based isolation. The default choice for local development on macOS and Windows.

json
{
  "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.

json
{
  "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).

json
{
  "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:

  1. remoteBuild.cscLink — base64-encoded credentials or a file path in the build config.
  2. SNAP_CSC_LINK — environment variable, same format (CI-recommended).
  3. SNAPCRAFT_STORE_CREDENTIALS — plain-text macaroon, read directly by snapcraft.
  4. An active interactive snapcraft login session.

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:

sh
# Run locally — output is your CI secret value
snapcraft export-login - | base64 -w0

Then set SNAP_CSC_LINK in your CI environment:

yaml
# 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):

json
{
  "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-2404 platform content snap (GTK3, GLib, etc.)
  • Adds gtk-3-themes, icon-themes, sound-themes content snaps
  • Adds the gpu-2404 GPU 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:

json
{
  "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:

json
{
  "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:

json
{
  "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.)

json
{
  "snapcraft": {
    "base": "core22",
    "core22": {
      "summary": "My Electron app",
      "confinement": "strict",
      "stagePackages": ["default"]
    }
  }
}

Key differences from core24:

  • allowNativeWayland defaults to false — Wayland is disabled by default for older Electron compatibility. Set to true to enable. (The legacy interface uses allowNativeWayland; core24 uses forceX11.)
  • No extensions — no GNOME extension support; uses the desktop-gtk2 part instead.
  • useTemplateApp — when stagePackages is 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 the mksquashfs from the toolsets.appimage bundle (non-template builds run the snapcraft CLI). compression: "lzo" needs a toolset other than the legacy "0.0.0", whose mksquashfs supports only gzip and xz.

Core 20

core20 targets Ubuntu 20.04 Focal.

json
{
  "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.

json
{
  "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.

json
{
  "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.

jsonc
// 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:

yaml
snapcraft:
  base: core22
  core22:
    appPartStage:
      - -usr/share/doc
      - -usr/share/man

Publishing to the Snap Store

  1. Create an account at snapcraft.io, register your snap name, and install the snapcraft CLI — see Publishing to the Snap Store
  2. Configure publishing in your electron-builder config:
yaml
publish:
  provider: snapStore
  repo: myapp       # your registered snap name
  channels:
    - stable
  1. 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:

bash
sudo snap install --dangerous dist/myapp_1.0.0_amd64.snap

Run and check logs:

bash
myapp
journalctl --user -xe | grep myapp

To uninstall: sudo snap remove myapp

Debugging

Enable verbose electron-builder output:

bash
DEBUG=electron-builder electron-builder --linux snap

For snap runtime issues, check confinement denials:

bash
snap run --shell myapp
# Inside the snap shell, test what's accessible

Configuration

`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 (.nsh hooks) — 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 .msixupload artifacts
  • Adds modern manifest features: package integrity and Windows services
  • Requires the modern winCodeSign toolset (the default; only the legacy 0.0.0 bundle 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

text
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: Portable

macOS 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 /Applications shortcut
  • 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 scripts directory
  • 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

text
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 apt from a repository or directly
  • Best when targeting Ubuntu users who expect .deb packages
  • See Linux Configuration → DEB

RPM — use for Red Hat-based distribution (Fedora, RHEL, CentOS, openSUSE).

Snap — use for Ubuntu-primary distribution via the Snap Store.

  • Automatic updates via Snap Store
  • Sandboxed with strict confinement
  • Required for Snap Store listing
  • See Snap Configuration

Flatpak — use for cross-distro sandboxed distribution.

  • Works on any distro with flatpak installed
  • electron-builder produces single-file bundles (not Flathub repository packages)
  • See Flatpak Configuration

Pacman — use for Arch Linux and Arch-based distributions.

APK — use for Alpine Linux or Docker container base images.

Linux Decision Tree

text
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?
  → APK

Combining Multiple Targets

You can build multiple targets in a single run:

yaml
# electron-builder.yml
win:
  target:
    - nsis
    - portable

mac:
  target:
    - dmg
    - zip

linux:
  target:
    - AppImage
    - deb
bash
# 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 --arm64

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]

  • wine is only needed to build Windows targets on a non-Windows machine. On Windows it has no effect. It defaults to the host-installed wine on PATH ("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.
  • winCodeSign is used on all platforms (signtool.exe on Windows, osslsigncode on macOS/Linux).
  • squirrel is only used by the squirrelWindows target. It runs natively on Windows; on macOS/Linux it needs a host-installed mono, and rcedit (from winCodeSign) runs under Wine.
  • fpm, icons, and squirrel each 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:

json5
{ "build": { "toolsets": {} } }                          // unset → latest
{ "build": { "toolsets": { "nsis": "latest" } } }        // explicit latest

:::note
null 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:

yaml
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:

json5
{
  "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:

typescript
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. Relative file:// paths must resolve inside the project's build-resources directory; absolute paths are used directly.
json5
// 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 in toolsets.<name>.checksum.
  • Why SHA-256 hex is the default: v27 downloads toolsets with the official @electron/get package, whose checksum verification (sumchecker) only supports SHA-256 hex. electron-builder verifies a base64 SHA-512 itself.
  • Not accepted: prefixed forms such as sha256:… or sha512-…, 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 a file:// archive file. A bare file:// 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 —
json5
{ "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.
  • .zip and .7z are 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 as nsis-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 }
json5
{ "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 modern signtool / osslsigncode (native arm64) plus the Windows Kits used for AppX/MSIX. The hsm and pkcs11 signing modes require this modern bundle.
  • Azure Trusted Signing (win.sign: { type: "azure" }) uses the faster signtool /dlib path out of the box, because the default winCodeSign ships the ATS dlib + .NET 8 payload — no pin needed. To force the legacy PowerShell Invoke-TrustedSigning path, pin winCodeSign below 1.3.0 (e.g. "1.2.1" or "0.0.0"). A ToolsetCustom object uses the dlib from 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:

bash
DEBUG=electron-builder electron-builder build

Code 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:

yaml
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:

bash
npx asar list dist/mac/MyApp.app/Contents/Resources/app.asar | sort

Common culprits: TypeScript source files (.ts), source maps (.map), test fixtures, large binary assets. Add exclusion patterns to files:

yaml
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:

yaml
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:

bash
# Ubuntu/Debian
sudo apt install fuse libfuse2

# Fedora
sudo dnf install fuse fuse-libs

Or run with --appimage-extract-and-run:

bash
./MyApp.AppImage --appimage-extract-and-run

AppImage 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:

bash
flatpak install flathub org.freedesktop.Platform//24.08
flatpak install flathub org.freedesktop.Sdk//24.08

Snap 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:

bash
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.log to 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.

json
"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.Subject prints it in the form to use as-is.
  • macOS / Linux: osslsigncode verify -in <App>.exe — the Subject: line under "Signer's certificate", printed as /C=US/ST=California/O=My Company, Inc./CN=My Company, Inc.. Keep only CN, O and C and write them as CN="My Company, Inc.", O="My Company, Inc.", C=US: OpenSSL names some other components differently from Windows (ST instead of S, 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:

js
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:

ts
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.

ts
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).

  1. Open Parallels Desktop.
  2. File -> New.
  3. Select "Modern.IE" in the "Free Systems".
  4. Continue, Continue, Accept software license agreement.
  5. Select "Microsoft Edge on Windows 10".
  6. 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.

  1. Download a Windows evaluation ISO from the Microsoft Evaluation Center (Microsoft no longer publishes the prebuilt "MSEdge on Win10" virtual machines).
  2. 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

  1. 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.
  2. zip target for macOS is required for Squirrel.Mac, otherwise latest-mac.yml cannot be created, which causes autoUpdater error. Default target for macOS is dmg+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

  1. Install electron-updater as an app dependency.

  2. Configure the publish options depending on where you want to host your release files.

  3. Build your application and check that the build directory contains the metadata .yml files 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.

  4. Use autoUpdater from electron-updater instead of electron:

    CommonJS

    js
    const { autoUpdater } = require("electron-updater")

    ESM

    js
    import { autoUpdater } from "electron-updater"

    TypeScript

    typescript
    import 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;
    }
  5. Call autoUpdater.checkForUpdatesAndNotify(). Or, if you need custom behaviour, implement electron-updater events, 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]

typescript
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()
  }
}

:::

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.

typescript
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:

typescript
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.

ts
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 = null

A 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:

  1. 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: powerMonitor shutdown on macOS/Linux, and the BrowserWindow session-end event on Windows (windowless, e.g. tray-only, apps cannot be covered on Windows).

  2. 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:

    js
    autoUpdater.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 allowDowngrade is 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 call quitAndInstall() yourself).

    A single quit can also be deferred without setting the property:

    js
    autoUpdater.quitAndInstall({ isSilent: true, isForceRunAfter: true, waitUntilNextLaunch: true })

    You can also trigger the pending install explicitly (e.g. before opening your first window):

    js
    await 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).

js
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:

js
autoUpdater.forceDevUpdateConfig = true

:::note
If you see this in logs:

text
APPIMAGE env is not defined, current application is not an AppImage

you need to apply this workaround otherwise update won't continue:

js
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.0 latest-mac.json
  • 2.15.0 path
  • 2.16.0 files

:::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:

ts
import { NsisUpdater } from "electron-updater"
const updater = new NsisUpdater()
updater.disableWebInstaller = false // only if you intentionally ship a web installer

Set 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:

ts
import { autoUpdater } from "electron-updater"
autoUpdater.allowUnverifiedLinuxPackages = false

Staged 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).

yml
version: 1.1.0
files:
  - url: TestApp Setup 1.1.0.exe
    sha512: Dj51I0q8aPQ3ioaz9LMqGYujAYRbDNblAQbodDRXAMxmY6hsHqEl3F6SvhfJj5oPhcqdX1ldsgEvfMNXGUXBIw==
    size: 62021782
stagingPercentage: 10

Update 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`

  • error Error

Emitted when there is an error while updating.

Event: `checking-for-update`

Emitted when checking if an update has started.

Event: `update-available`

  • info UpdateInfo (for generic and github providers)
    Emitted when there is an available update. The update is downloaded automatically if autoDownload is true.

Event: `update-not-available`

Emitted when there is no available update.

  • info UpdateInfo (for generic and github providers)

Event: `download-progress`

  • progress ProgressInfo
    • bytesPerSecond
    • percent
    • total
    • transferred

Emitted on progress.

Event: `update-downloaded`

  • info UpdateInfo — for generic and github providers.

Event: `update-cancelled`

Emitted when an update is cancelled by the user or the download is otherwise aborted.

Event: `appimage-filename-updated`

  • path string

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

text
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 providers

Phase-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
js
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
js
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)
js
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
js
electronDist: async (context) => {
  return "/path/to/custom-electron-dist"
}

Phase 2d — Copy App Files

This is the main packaging phase. electron-builder:

  1. Computes the file set based on the files glob patterns (defaulting to everything except node_modules and dev artifacts).
  2. Filters node_modules — only production deps are included by default.
  3. Creates an ASAR archive (enabled by default). In v27 the asar: true sentinel was removed — omit the key to keep defaults, pass an asar: {} object to configure it, or asar: false to disable.
  4. Copies extraResources into the platform resources directory.
  5. Copies extraFiles into 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
js
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
js
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 VERSION or BUILD_ID file into the bundle
  • Modify Info.plist (macOS) or resources/app.asar contents
  • 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.

See: Adding Electron Fuses


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
js
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):

yaml
# 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
js
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
js
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
js
// 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)
}
js
// 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
js
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.

yaml
# electron-builder.config.yml
concurrency:
  jobs: 2   # pack up to 2 platform/arch combos simultaneously

What 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/x64 and mac/arm64 start concurrently (2 slots)
  • win/x64 starts as soon as one of the mac slots finishes
  • afterAllArtifactBuild fires 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.

yaml
concurrency:
  jobs: 4   # safe upper bound on most systems

Hook 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


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

bash
npm install --save-dev electron-forge-maker-nsis
js title="forge.config.js"
const { 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

bash
npm install --save-dev electron-forge-maker-nsis-web
js title="forge.config.js"
const { 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

bash
npm install --save-dev electron-forge-maker-appimage
js title="forge.config.js"
const { 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

bash
npm install --save-dev electron-forge-maker-snap
js title="forge.config.js"
const { 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 --publish directly
  • 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. Use GITHUB_TOKEN (provided automatically) or a PAT with repo scope

Minimal Multi-Platform Workflow

This workflow builds for macOS, Windows, and Linux on every push to main and on version tags:

yaml
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):

yaml
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:

yaml
# In electron-builder.yml
publish:
  provider: github
  releaseType: draft

Then in GitHub: go to Releases → find the draft → click "Edit" → "Publish release".

macOS Code Signing Setup

Preparing the Certificate

bash
# 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 clipboard

Add to GitHub Secrets:

  • MAC_CSC_LINK — the base64-encoded .p12 content
  • MAC_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:

yaml
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

bash
# 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.txt

Add to GitHub Secrets:

  • WIN_CSC_LINK — base64-encoded .pfx content
  • WIN_CSC_KEY_PASSWORD — certificate password

Using Azure Trusted Signing

yaml
- 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:

yaml
- 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 -):

yaml
- 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:

text
/* 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.


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 binary
  • msiProjectCreated — edit WiX XML before MSI compilation
  • appxManifestCreated — edit AppX manifest XML before packaging
  • onNodeModuleFile — filter which node_modules files are included in the bundle

Defining Hooks

Hooks can be defined in three ways:

1. Inline Function (JS/TS config only)

js
// 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:

yaml
# electron-builder.yml
afterPack: ./scripts/afterPack.js
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:

yaml
afterPack: my-electron-builder-plugin

Hook Reference

`beforeBuild`

Runs before native dependencies are installed or rebuilt (before npm rebuild). Resolving to false skips dependency installation entirely.

js
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.

js
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.

js
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
js
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.

js
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.

js
artifactBuildStarted: async (context) => {
  // context: ArtifactBuildStarted
  // { targetPresentableName, file, safeArtifactName, packager, arch }
  console.log("Building artifact:", context.targetPresentableName)
}

`artifactBuildCompleted`

Runs when an individual artifact finishes building.

js
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.

js
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.

js
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.

js
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.

js
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.

js
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

js
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

js
mac:
  sign: "./scripts/sign.js"
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

js
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

js
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:

js
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:

bash
DEBUG=electron-builder electron-builder build

Interface 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
:::

text
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.png

A 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:

  1. The explicit mac.icon, win.icon, or linux.icon config path
  2. icon.<format> (e.g. icon.icns for macOS, icon.ico for Windows)
  3. icon.png
  4. icon.svg — auto-discovered, no config required
  5. icon.icns (cross-platform source for ICNS-based conversion)
  6. A directory named icons/ or icon/

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 (set format): The SVG is passed through directly to the freedesktop scalable/ icon hierarchy. Desktop environments render it at any scale — ideal for HiDPI and fractional-scaling displays.

Explicit config also works:

yaml
mac:
  icon: build/icon.svg

win:
  icon: build/icon.svg

macOS 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:

yaml
linux:
  icon: build/icons/
text
build/icons/
├── 16x16.png
├── 32x32.png
├── 48x48.png
├── 128x128.png
├── 256x256.png
└── 512x512.png

Files 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)
yaml
dmg:
  background: build/background.png
  backgroundColor: "#ffffff"   # fallback if no background image
  icon: build/dmg-icon.icns    # volume badge icon (optional)
  iconSize: 80
  iconTextSize: 12

Supply 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
yaml
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):

json5
{ "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:

  1. An install trusts a LIST of keys. updateManifestPublicKey in app-update.yml is 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.
  2. A manifest may carry several signatures. Every configured signing key signs each latest*.yml; the result is written as a signatures list (one { keyId, signature } entry per key) plus the legacy single signature field holding the first key's signature. keyId is the hex SHA-256 of the public key's SPKI DER encoding (printed by create-update-key), so you can see at a glance which keys signed a release.
  3. 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, and ELECTRON_BUILDER_UPDATE_SIGN_KEY_FILE accepts several paths joined with the OS path delimiter (: on Linux/macOS, ; on Windows). Relative signingKeyFile paths resolve against the project directory; relative ELECTRON_BUILDER_UPDATE_SIGN_KEY_FILE paths against the current working directory. Setting updateManifest to false disables signing outright and short-circuits this whole ladder, environment variables included.
  4. The embedded trust list is either explicit or derived. If updateManifest.publicKey is 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.
:::

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:

yaml
# 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:

sh
npx electron-builder create-update-key --out ./update-private-key.new.pem

The 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:

sh
# 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 always

Put 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.yml on a different channel) 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.updateManifestPublicKey to a list explicitly; this overrides the value from app-update.yml, which helps if the trust list must change without rebuilding app-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.

  1. 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.
  2. 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.
  3. 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.
  4. 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 with ERR_UPDATER_MANIFEST_NOT_SIGNED.

Key storage

  • Never commit private keys. updateManifest.signingKey exists for completeness; in practice use ELECTRON_BUILDER_UPDATE_SIGN_KEY (PEM contents) or ELECTRON_BUILDER_UPDATE_SIGN_KEY_FILE (paths to mounted secret files) from your CI secret store.

  • Prefer the _FILE variant 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*.yml in a post-publish step of your own (append an entry to signatures with the matching keyId, or set signature, over the payload from canonicalizeForSigning in builder-util-runtime), and list the public key in updateManifest.publicKey so it is embedded. electron-builder warns at build time when none of the keys it signs with is in an explicit publicKey list, since such a release cannot verify its own manifests.

    A publicKey alone 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*.yml locally; your step signs it and publishes the artifacts and the manifest.
    • Keep publishing, with publishAutoUpdate: false on every publish provider. electron-builder uploads the artifacts and embeds the publicKey, but writes no latest*.yml at all, so your step has to generate the manifest as well as sign and upload it.
  • Pre-signed download URLs (files[].url, files[].blockMapUrl, packages.<arch>.path) are covered by the signature, so a step that writes them into latest*.yml has 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 bare CN= 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.yml has no publisherName at 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=, or C= 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:

yaml
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 (or certificateSubjectName/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 publisherName for PKCS#11 without an extractable certificate, an HSM/PKCS#11 .crt/.cer without a CN, or HSM with a custom sign hook and no certificate identifier.
  • Custom sign hook — electron-builder cannot read the certificate the hook signs with, so set publisherName (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 publisherName is required and is embedded verbatim. Confirm the Subject Azure signs with before the bridge release.
  • Custom verifyUpdateFileAuthenticodeSignature function — 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 via CSC_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.yml exactly 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.yml is the integrity layer electron-builder provides for every Linux target, and it rotates as described above. The downloaded file's sha512 is 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 (default true) controls whether the package manager's own signature checks are bypassed when a downloaded package is installed. With false, apt, dnf/yum, and zypper enforce their normal verification; plain dpkg performs no signature verification at all, and a bare rpm install 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:

  1. Publish the new public key in your repository metadata and on your download site.
  2. 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.
  3. 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.
  4. 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.yml lists both keys under updateManifestPublicKey / both publisherName entries, and latest*.yml has two signatures entries, before publishing.
  • Never reach for updateManifest: false to 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 publicKey entry, 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.

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:

bash
brew install rpm

Linux

You can use Docker to avoid installing system dependencies.

To build in distributable format:

bash
sudo apt-get install --no-install-recommends -y libopenjp2-tools

To build rpm:

bash
sudo apt-get install --no-install-recommends -y rpm
# or
sudo yum install rpm-build

To build pacman:

bash
sudo apt-get install --no-install-recommends -y bsdtar

To build snap (only needed with custom stage packages):

bash
sudo snap install snapcraft --classic
sudo snap install multipass --beta --classic

Build 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

bash
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

sh
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:wine

Then 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*.yml update 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: false is the only opt-out and warns on every build. Unlike the sha2-only metadata grace period below, this one is enforced in v27. See Signed Update Manifests.

  • Web-installer payloads are disabled by default. AppUpdater.disableWebInstaller now defaults to true. 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 with ERR_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 an nsis-web installer built with electron-builder v27+ opt in automatically (via the resources/package-type marker); set autoUpdater.disableWebInstaller = false yourself only for web-installer installs without that marker (built before v27, or with a custom script) or when switching an app from nsis to nsis-web. Cached web updates are re-verified against the freshly fetched manifest, and the nsis-web installer 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 set updateManifest: false for nsis-web apps.

  • Update credentials stay on the feed's origin. The credential headers from 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 update feed's origin. A download on another origin, including the same host on another port or over http instead of https, is requested without them. See Auto Update.

  • Linux package signatures. AppUpdater.allowUnverifiedLinuxPackages (default true) preserves historical behavior, since electron-builder does not sign Linux packages itself. Set it to false to enforce GPG signature checks when installing .deb / .rpm auto-updates on package managers that support verification.

  • Modern, verified update metadata. Generated latest*.yml targets the files[] format and the default electronUpdaterCompatibility is now >=2.16. Update metadata validated only by the legacy SHA-256 sha2 checksum is deprecated.

  • Windows update signature verification. On Windows, downloaded NSIS updates are Authenticode-verified before they are applied. win.verifyUpdateCodeSignature (default true) embeds your signing publisherName into app-update.yml, and electron-updater checks each downloaded update against the certificate subject. Supply an array of publisherName values 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.asar archive (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: true disables 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). Pairing enableEmbeddedAsarIntegrityValidation with onlyLoadAppFromAsar binds 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.sign discriminated 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.

bash
electron-builder --publish always   # explicit — required in v27

See 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:

text
• 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-app

That 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.

json5
{
  "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/signatures fields 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):

sh
npx electron-builder create-update-key

This 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:

sh
# 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 always

No 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:

text
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:

yaml
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: null

The whole option may instead be set to false, the only opt-out:

yaml
updateManifest: false # publishes UNSIGNED manifests; warns on every build

Key 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:

ts
import { autoUpdater } from "electron-updater"
autoUpdater.updateManifestPublicKey = "-----BEGIN PUBLIC KEY-----\n…" // or an array of keys

Opting 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.

yaml
updateManifest: false

Two properties are worth knowing:

  • It short-circuits the environment variables. A leftover ELECTRON_BUILDER_UPDATE_SIGN_KEY in CI cannot re-enable signing behind the back of the opt-out, so false means 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 of null, 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:

yaml
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:

  1. Bridge release(s): configure both the old and the new private key (old first, so it also fills the legacy signature field), e.g. ELECTRON_BUILDER_UPDATE_SIGN_KEY_FILE=old.pem:new.pem. Manifests are now dual-signed and app-update.yml trusts [old, new]. Installs in the field verify via the old signature; once updated they trust both keys.
  2. Keep dual-signing for as long as you want installs that predate the first bridge release to be able to update.
  3. 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.

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:

  1. Run the automated migrator — rewrites your config to v27 form
  2. Update Node.js — to >=22.12.0
  3. 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:

bash
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-schema rewrites a .json5 file it produces valid JSON (no comments, standard quoting) and prints a warning. If preserving comments matters, apply the changes manually.
  • TOML: the toml npm package is read-only. migrate-schema detects TOML configs, prints the required changes, and exits without writing.
  • YAML: js-yaml preserves 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 when typescript is not installed.
  • snap base defaulting: when your old snap config has no base field, 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:

bash
# nvm
nvm install 22 && nvm use 22

# fnm
fnm install 22 && fnm use 22

Update CI (GitHub Actions):

yaml
- uses: actions/setup-node@v4
  with:
    node-version: '22'

Update your package.json engines field if you declare one:

json
{ "engines": { "node": ">=22.12.0" } }

Confirm your CI and Docker images run Node.js 22.12 or later:

dockerfile
FROM node:22-bookworm-slim

electron-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

js
// 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:


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 .customNsisBinary was 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
  • The NSIS Toolset Bundle you download is a valid toolsets.nsis bundle and provides easy locations you can drop any makensis binaries you into.
    • a. /windows/ ← Copy /Bin/makensis.exe From your .customNsisBinary archive to this path to migrate
    • b. /mac/x64 ← Copy /mac/makensis from a .customNsisBinary archive to this path to migrate
    • c. /mac/arm64
    • d. /linux/x64 ← Copy /linux/makensis from a .customNsisBinary archive to this path to migrate
    • e. /linux/arm64
  • 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.nsis bundle now provides native makensis binaries for newly supported targets like Mac (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 a makensis/makensis.exe executable for the OS & arch combos you intend to run your builds with.
  • (optional) - The new toolsets.nsis is more flexible than previous .customNsisBinary/.customNsisResources in 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
  • Include your newly created bundle in your electron builder config via toolsets.nsis by either:
    • a. Providing a file:// as the toolsets.nsis.url.
    • b. Similar to how .customNsisBinary/.customNsisResources worked - host the bundle as a .7z file remotely. Provide the remote url as the toolsets.nsis.url & and provide the hash of the archive (either sha256-hex or sha512-base64) to the toolsets.nsis.checksum property.

**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
  • The NSIS Toolset Bundle you download is a valid toolsets.nsis bundle 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
  • 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 makensis binary runtime are /Stubs, /Include, /Contrib, /Plugins. Nothing outside of /plugins was 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
  • (optional) - The new toolsets.nsis is more flexible than previous .customNsisBinary/.customNsisResources in 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
  • Include your newly created bundle in your electron builder config via toolsets.nsis by either:

    • a. Providing a file:// as the toolsets.nsis.url.
    • b. Similar to how .customNsisBinary/.customNsisResources worked - host the bundle as a .7z file remotely. Provide the remote url as the toolsets.nsis.url & and provide the hash of the archive (either sha256-hex or sha512-base64) to the toolsets.nsis.checksum property.
  • The toolsets.nsis bundle must carry elevate.exe at 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.nsis documentation

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-version updated to '22'
  • "engines" field in package.json updated (if declared)

Build config — auto-migrated by migrate-schema

  • ✓ electronCompile removed (if present)
  • ✓ disableDefaultIgnoredFiles removed (if present; re-include specific files via a files glob such as **/*.obj)
  • ✓ framework, nodeVersion, launchUiVersion removed (if present)
  • ✓ npmSkipBuildFromSource → nativeModules.buildDependenciesFromSource (if present)
  • ✓ buildDependenciesFromSource, nodeGypRebuild, npmRebuild moved into nativeModules; nativeRebuilder → rebuildMode
  • ✓ Legacy asar keys + asarUnpack → asar.unpack (platform-level too); disableSanityCheckAsar → asar.disableSanityCheck; disableAsarIntegrity → asar.disableIntegrity; asar: true removed
  • ✓ macOS signing fields → mac.sign.*; signIgnore → sign.ignore (also mas/masDev)
  • ✓ mac.universal fields → mac.universal.*
  • ✓ electronDownload → electronGet (verify dropped cache/customDir/customFilename/strictSSL/platform/arch/version/force)
  • ✓ appImage.systemIntegration removed (if present)
  • ✓ linux.syncDesktopName removed (if present; always synced in v27 — set desktopName if you had it false)
  • ✓ GithubOptions.vPrefixedTagName → tagNamePrefix (if present)
  • ✓ win.azureSignOptions / win.signtoolOptions → win.sign; win.signExecutable / win.signAndEditExecutable handled
  • ✓ snap restructured to snapcraft (verify the assumed base if none was set)
  • ✓ helper-bundle-id → mac.helperBundleId; squirrelWindows.noMsi → msi; root-level directories → build.directories
  • ✓ toolsets.*: null removed; toolsets.appimage: "1.0.2" → "1.0.3"; nativeRebuilder: "legacy" dropped

Runtime defaults & auto-update

  • DMG filesystem default is now APFS — set dmg.filesystem: "HFS+" only for pre-10.13 macOS compatibility
  • macOS default entitlements now grant only allow-jit — add allow-unsigned-executable-memory / disable-library-validation back in build/entitlements.mac.plist (and .inherit.plist for nested binaries) only if your app loads foreign-signed or unsigned binaries; MAS builds without build/entitlements.mas.plist now get osx-sign's sandboxed default
  • electron-updater disableWebInstaller defaults to true — web-installer updates are rejected unless it is false; v27+ nsis-web installs opt in automatically, set disableWebInstaller: false for older web-installer installs or when switching from nsis to nsis-web; the nsis-web installer verifies its app package (nsisWeb.allowUnverifiedAppPackage opts out)
  • A code-signed Windows build that writes app-update.yml needs a publisher name — set win.sign.publisherName for a custom win.sign.sign hook or a certificate without a CN, or win.verifyUpdateCodeSignature: false if your updates are not Authenticode-signed
  • NSIS per-machine builds set isAdminRightsRequired — their updates are started with elevate.exe and not installed automatically at launch; with autoInstallEvent: "onNextLaunch", call installPendingUpdateIfAvailable()
  • Update credentials stay on the feed's origin — requestHeaders / addAuthHeader credentials and the feed URL's query are not sent to downloads on another origin
  • latest*.yml drops legacy top-level path/sha512; default electronUpdaterCompatibility is now >=2.16 — read files[] instead; pin a legacy-inclusive range only for legacy embedded updaters
  • Production dependencies that electron-builder provides or that your bundler inlines (electron/electron-builder/react/…) are excluded from the copied node_modules instead of erroring; ALLOW_ELECTRON_BUILDER_AS_PRODUCTION_DEPENDENCY removed and electron-prebuilt/electron-rebuild no longer guarded — tune via ignoredProductionDependencies
  • Building Windows targets on macOS uses the host Wine (toolsets.wine defaults 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 by migrate-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.customSquirrelVendorDir and a custom NSIS bundle in nsis.customNsisBinary / customNsisResources, are reported with a warning. Runtime, CLI, env-var, and behavior changes require manual action — see what migrate-schema does and does not do.

:::tip[Run the automated migrator first]

bash
electron-builder migrate-schema            # apply changes in place
electron-builder migrate-schema --dry-run  # preview only

This 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-installed wine on PATH, 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, osslsigncode 2.11 (native arm64), and the Azure Trusted Signing dlib + .NET 8 payload.
  • appimage → 1.1.0 — static FUSE3-compatible runtime; adds unsquashfs support.
  • icons → 1.2.3 — newer wasm-vips / @resvg/resvg-wasm bundle that writes 16/32 px ICNS entries as ARGB (was 1.1.0).
  • linuxToolsMac → 1.0.1 (runs on macOS 15+) and sevenZip → 1.0.1 (correct per-arch Windows binaries).
  • nsis (1.2.1) and fpm (2.2.1) are unchanged; the new squirrel toolset resolves to 1.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.

json
{
  "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:

js
// 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.

json5
{ "build": { "electronCompile": true } }  // ← delete this line

If 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).

json5
{
  "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:

json5
{
  "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:

json5
// 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:

json5
{ "build": { "asar": true } }  // Still works — but redundant
{ "build": { "asar": {} } }    // Equivalent; or omit the key entirely

migrate-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.

json
{ "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.

json5
{ "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.

json5
{ "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 customSquirrelVendorDir pointed at a directory whose contents were the vendor files (Squirrel.exe, nuget.exe, …) and was copied verbatim, bypassing all provisioning.
  • toolsets.squirrel accepts a version pin ("1.1.1" / "latest") or a ToolsetCustom object whose url points at a bundle that contains an electron-winstaller/vendor/ subtree. The bundle still goes through normal provisioning: on every platform rcedit.exe is supplied from the winCodeSign toolset (it runs under Wine on non-Windows hosts), and for msi: true the shared WiX toolset is merged in.
json5
// 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) in v26, now you must provide that binary via a toolsets.nsis bundle for parity on v27.
  • Custom NSIS Resources Custom NSIS resources are required to expose any additional 3rd party plugins that may be needed in custom .nsh scripts. If you were using (.customNsisResources) in v26, now you must provide that resource via a toolsets.nsis bundle for parity on v27.
  • customNsisBinary.debugLogging moves to nsis.installerDebugLogging (or nsisWeb.installerDebugLogging). It never had an effect on portable, which has no equivalent.
json5
// 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.

json5
{ "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.

json5
// Before (v26) — opt-in
{ "build": { "linux": { "syncDesktopName": true } } }  // ← delete this line

migrate-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).

ts
// 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.

json5
// 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
json5
// 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.)

json5
// 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.asar on 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 InvalidConfigurationError when 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)
json5
// 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, .node modules, executables in app.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 supply build/entitlements.mac.inherit.plist / mac.sign.entitlementsInherit.
  • MAS builds without a build/entitlements.mas.plist fall back to @electron/osx-sign's sandboxed default.mas.plist (previously they received the non-sandboxed mac template).
  • Ad-hoc builds (mac.sign.identity: "-") keep working: a built-in ad-hoc entitlements file that includes disable-library-validation is 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.unpacked and Contents/PlugIns (which electron-builder never re-signs) whose signature carries a different Team ID (or none) while disable-library-validation is 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
json5
{ "mac": { "mergeASARs": true, "singleArchFiles": "*.node" } }                       // Before
{ "mac": { "universal": { "mergeASARs": true, "singleArchFiles": "*.node" } } }      // After

Windows 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:

typescript
win.sign: { type: "signtool" | "hsm" | "pkcs11" | "azure", … } | false | null

false / 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".

json5
{ "win": { "signtoolOptions": { "certificateFile": "cert.pfx", "publisherName": "CN=ACME Inc" } } }              // Before
{ "win": { "sign": { "type": "signtool", "certificateFile": "cert.pfx", "publisherName": "CN=ACME Inc" } } }     // After

win.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.

json5
// 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; requires toolsets.winCodeSign: "1.x").
  • type: "pkcs11" — PKCS#11 token via osslsigncode (cross-platform; runs on macOS/Linux CI without a Windows VM).
json5
{
  "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:

json5
{ "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

:::warning
win.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)
json5
{ "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.

json5
// 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