Fix: Electron Forge Not Working — Makers, Code Signing, Native Modules, and Publishers
Part of: Docker, DevOps & Infrastructure
Quick Answer
How to fix Electron Forge errors — forge.config.js makers per OS, code signing on macOS (notarytool) and Windows, native module rebuild via electron-rebuild, Vite/Webpack plugin, auto-updater, and GitHub publisher.
The Error
You run npm run make and Forge complains about missing makers:
An unhandled rejection has occurred inside Forge:
Error: No makers configured for platform "win32"Or macOS notarization fails:
notarytool: 401 Unauthorized
The session token has expired.Or a native module crashes at runtime:
Error: The module '/path/to/better_sqlite3.node' was compiled against a
different Node.js version using NODE_MODULE_VERSION 119.
This version of Node.js requires NODE_MODULE_VERSION 121.Or Windows users see a SmartScreen warning:
Windows protected your PC
Microsoft Defender SmartScreen prevented an unrecognized app from starting.Why This Happens
Electron Forge unifies several tasks: bundling (Webpack/Vite), packaging (asar), and producing platform-specific installers (Squirrel for Windows, DMG for macOS, deb/rpm for Linux). Where Electron Builder is a single monolithic CLI, Forge is a plugin architecture — each maker, packager, and publisher is a separate package configured in forge.config.js. That modularity is the source of most “missing maker” and “wrong native module ABI” errors.
The bigger conceptual hurdle is that desktop distribution is genuinely three problems wearing one hat: building the JavaScript, packaging it with an Electron runtime, and producing a signed installer the OS will trust. Forge runs the build through Vite or Webpack, packages the result via @electron/packager (asar-archived), and then hands the packaged app to each maker for installer creation. Anything that fails downstream — code signing, notarization, native module ABI mismatch — usually traces back to a config gap in one of those three handoffs.
- Per-OS makers. Each target OS needs a “maker” plugin. Without it,
makeproduces nothing for that OS. - Code signing is OS-specific. macOS requires a Developer ID + notarization via Apple’s notarytool. Windows requires a code-signing certificate (or you ship unsigned with SmartScreen warnings).
- Native modules use Node-API or NAN. They’re compiled against a specific Node ABI. Electron uses its own bundled Node version — modules built for system Node won’t load.
- Publishers automate distribution. Without one,
makeproduces installers but doesn’t upload them anywhere.
Fix 1: Configure Makers Per Platform
forge.config.js:
module.exports = {
packagerConfig: {
name: "MyApp",
executableName: "myapp",
icon: "./assets/icon", // No extension — Forge picks .ico, .icns, .png per OS
asar: true,
},
makers: [
// macOS
{
name: "@electron-forge/maker-dmg",
config: {
icon: "./assets/icon.icns",
format: "ULFO",
},
},
// Windows
{
name: "@electron-forge/maker-squirrel",
config: {
name: "myapp",
setupIcon: "./assets/icon.ico",
certificateFile: process.env.WINDOWS_CERT_FILE,
certificatePassword: process.env.WINDOWS_CERT_PASSWORD,
},
},
// Linux
{
name: "@electron-forge/maker-deb",
config: {
options: {
icon: "./assets/icon.png",
maintainer: "Your Name",
homepage: "https://example.com",
},
},
},
{
name: "@electron-forge/maker-rpm",
config: {
options: {
icon: "./assets/icon.png",
},
},
},
// Cross-platform ZIP (useful fallback)
{
name: "@electron-forge/maker-zip",
platforms: ["darwin", "linux", "win32"],
},
],
plugins: [
{
name: "@electron-forge/plugin-vite",
config: {
build: [
{ entry: "src/main.ts", config: "vite.main.config.ts" },
{ entry: "src/preload.ts", config: "vite.preload.config.ts" },
],
renderer: [
{ name: "main_window", config: "vite.renderer.config.ts" },
],
},
},
],
};make runs each maker that matches the current OS:
npm run make # Makers for current OS only
npm run make -- --platform=darwin # Force macOS
npm run make -- --platform=win32 # Force WindowsBuilding for an OS you’re not on (e.g. Mac DMG from Linux) often fails — most CIs build per-OS in a matrix.
Pro Tip: Use the GitHub Actions matrix pattern to build for macOS / Windows / Linux in parallel. Each runner is the native OS; signing works correctly.
Fix 2: macOS Code Signing and Notarization
For macOS distribution outside the App Store:
- Apple Developer account ($99/year).
- Developer ID Application certificate (download from Apple Developer → Certificates).
- App-specific password for notarization (appleid.apple.com → Sign-In and Security → App-Specific Passwords).
- Keychain access for the cert (or set as env vars in CI).
forge.config.js:
module.exports = {
packagerConfig: {
osxSign: {
identity: "Developer ID Application: Your Name (TEAMID12345)",
"hardened-runtime": true,
entitlements: "./build/entitlements.mac.plist",
"entitlements-inherit": "./build/entitlements.mac.plist",
"signature-flags": "library",
},
osxNotarize: {
tool: "notarytool",
appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_ID_PASSWORD, // App-specific password
teamId: process.env.APPLE_TEAM_ID,
},
},
};build/entitlements.mac.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>These entitlements allow JIT (V8 engine) and network access. Add more for camera, mic, etc. if your app needs them.
In CI:
- name: Make
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: npm run makeNotarization takes 5-30 minutes. Forge waits for the response.
Common Mistake: Using your Apple ID password (not the app-specific password). Apple requires the app-specific one — generate at appleid.apple.com.
For sandbox apps (Mac App Store):
osxSign: {
identity: "3rd Party Mac Developer Application: ...",
type: "distribution",
}Different identity, different requirements. Most apps ship outside the App Store with Developer ID.
Fix 3: Windows Code Signing
For Windows, get an EV (Extended Validation) or OV (Organization Validation) code-signing cert from DigiCert, Sectigo, etc. ($200-500/year).
In forge.config.js:
makers: [
{
name: "@electron-forge/maker-squirrel",
config: {
name: "myapp",
certificateFile: process.env.WINDOWS_CERT_FILE,
certificatePassword: process.env.WINDOWS_CERT_PASSWORD,
},
},
],For EV certs stored on a hardware token (USB dongle), use signtool with cloud signing services:
makers: [
{
name: "@electron-forge/maker-squirrel",
config: {
windowsSign: {
signWithParams: '/tr http://timestamp.digicert.com /td sha256 /fd sha256 /n "Your Org"',
},
},
},
],Or use Azure Trusted Signing (cloud-based, EV-equivalent reputation):
config: {
windowsSign: {
hookFunction: async (filePath) => {
await azureTrustedSign(filePath);
},
},
}Without code signing, Windows shows SmartScreen warnings until enough users download and “Run anyway.” With EV certs, the warning is bypassed from day one.
Pro Tip: For small budgets, ship unsigned and accept the SmartScreen warning initially. After ~3000 downloads with “Run anyway,” Microsoft’s reputation system whitelists your app. Or use Azure Trusted Signing — newer, cheaper than traditional EV.
Fix 4: Rebuild Native Modules
When you install a native module (better-sqlite3, sharp, keytar), it compiles against your system Node — not Electron. They’ll crash at runtime.
@electron-forge/plugin-auto-unpack-natives handles this:
plugins: [
{
name: "@electron-forge/plugin-auto-unpack-natives",
config: {},
},
],This excludes native modules from the asar archive (they can’t run from inside asar).
For rebuilding against Electron’s Node version:
npm install --save-dev @electron/rebuild
npx electron-rebuildForge auto-rebuilds during make, but for testing, run manually after npm install.
For postinstall:
{
"scripts": {
"postinstall": "electron-rebuild"
}
}Common Mistake: Using a Node-only library (assumes Node’s fs/net) in the renderer. Electron’s renderer (where your React/Vue runs) has access to Node only if nodeIntegration: true — which is insecure. Use IPC to call Node code from the main process.
For modern Electron, prefer contextBridge for IPC:
// preload.ts
import { contextBridge, ipcRenderer } from "electron";
contextBridge.exposeInMainWorld("api", {
saveFile: (content: string) => ipcRenderer.invoke("save-file", content),
});// main.ts
ipcMain.handle("save-file", async (event, content) => {
await fs.writeFile("/path", content);
return { ok: true };
});// renderer:
const result = await window.api.saveFile("hello");Fix 5: Publishers for Automated Distribution
After make produces installers, publishers upload them:
publishers: [
{
name: "@electron-forge/publisher-github",
config: {
repository: {
owner: "your-username",
name: "your-app",
},
prerelease: false,
draft: true, // Manual review before going live
},
},
],Run:
npm run publishForge builds and uploads to GitHub Releases. Combined with auto-update (Fix 6), this is the standard distribution pattern.
For S3:
publishers: [
{
name: "@electron-forge/publisher-s3",
config: {
bucket: "my-app-releases",
region: "us-east-1",
public: true,
},
},
],For multiple publishers (e.g. GitHub for users, S3 for auto-update):
publishers: [
{ name: "@electron-forge/publisher-github", config: {...} },
{ name: "@electron-forge/publisher-s3", config: {...} },
],Common Mistake: Token scopes. GitHub publisher needs repo scope for private repos, public_repo for public. Personal Access Token in GITHUB_TOKEN env var.
Fix 6: Auto-Update With electron-updater
For automatic updates, use electron-updater (separate from Forge):
npm install electron-updater// main.ts
import { autoUpdater } from "electron-updater";
app.whenReady().then(() => {
createWindow();
autoUpdater.checkForUpdatesAndNotify();
});
autoUpdater.on("update-available", () => {
// Notify user
});
autoUpdater.on("update-downloaded", () => {
autoUpdater.quitAndInstall();
});In package.json:
{
"build": {
"publish": {
"provider": "github",
"owner": "your-username",
"repo": "your-app"
}
}
}autoUpdater checks for new releases, downloads in the background, and prompts the user to restart.
For self-hosted update servers, use provider: "generic" and host the release files yourself.
Pro Tip: Test auto-update flow before shipping. Make a 1.0.0, ship it, then release 1.0.1 to verify the upgrade path. Test on a clean install — not from your dev machine.
Fix 7: Webpack vs Vite Plugin
Electron Forge supports both Webpack and Vite plugins. Vite is newer, faster:
plugins: [
{
name: "@electron-forge/plugin-vite",
config: {
build: [
{ entry: "src/main.ts", config: "vite.main.config.ts" },
{ entry: "src/preload.ts", config: "vite.preload.config.ts" },
],
renderer: [
{ name: "main_window", config: "vite.renderer.config.ts" },
],
},
},
],Three Vite configs:
vite.main.config.ts— main process. Node target.vite.preload.config.ts— preload script. Special target (CJS, no externals).vite.renderer.config.ts— renderer. Browser target, plus Electron-aware.
Example renderer config:
// vite.renderer.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
root: "./src/renderer",
});For HMR in development, npm start (which runs electron-forge start) launches Electron with the Vite dev server attached.
Fix 8: Common Project Structure
my-electron-app/
├── src/
│ ├── main.ts # Main process (window creation, IPC)
│ ├── preload.ts # Preload (context bridge)
│ └── renderer/
│ ├── index.html
│ ├── main.tsx # React entry
│ └── App.tsx
├── forge.config.js
├── vite.main.config.ts
├── vite.preload.config.ts
├── vite.renderer.config.ts
├── package.json
└── assets/
├── icon.icns
├── icon.ico
└── icon.pngIn package.json:
{
"main": "./dist/main/main.js",
"scripts": {
"start": "electron-forge start",
"package": "electron-forge package",
"make": "electron-forge make",
"publish": "electron-forge publish"
}
}main is the entry point for the main process — Forge writes it during build.
Common Mistake: Wrong main path. The bundle goes to dist/main/main.js (via Vite/Webpack config). If main in package.json doesn’t match, the app won’t launch after packaging.
Electron Forge vs Other Electron Build Tools
The Electron ecosystem has three competing build pipelines plus a handful of standalone tools. The differences are real once you hit code signing, multi-OS CI, or auto-update.
Electron Forge. Plugin-based, official Electron-team-maintained, integrates Vite/Webpack natively, has first-class publishers for GitHub, S3, and others. Cleanest path for new projects; the documentation assumes Forge. Sweet spot: teams starting fresh in 2024-2026 that want the official-blessed pipeline.
Electron Builder. The long-standing alternative — monolithic CLI, configured by a single build field in package.json. More mature auto-updater (electron-updater), broader maker support (NSIS, MSI, AppImage, Snap), more battle-tested code-signing edge cases. Slightly more opaque, but mature. Sweet spot: existing projects already on Builder, or anyone needing NSIS/MSI on Windows or AppImage on Linux.
electron-vite. Pure dev-experience tool: Vite for main, preload, and renderer. Doesn’t make installers itself — you pair it with Builder or Forge for packaging. Best HMR experience among the three. Sweet spot: developers prioritizing fast iteration; production build is delegated.
electron-packager. The low-level packager that Forge uses under the hood. Lets you script your own pipeline if Forge’s plugin model is too rigid. No makers, no publishers — you build those yourself. Sweet spot: heavy customization, monorepo build scripts.
Rough mapping: new project + Vite + GitHub releases → Forge. Existing project + Windows MSI/NSIS + delta updates → Builder. Fast HMR matters most → electron-vite + Builder. Custom CI pipeline → packager. Migrating from Builder to Forge is doable but the configs are very different — budget a day to rewrite forge.config.js.
Still Not Working?
A few less-obvious failures:
makesucceeds but installer crashes. Test the unpacked app first:npm run packagethen run fromout/. If it works, the issue is in the installer creation, not the app code.- Native module not found at runtime. Add to
packagerConfig.extraResourceor useauto-unpack-natives. SyntaxError: Unexpected tokenin preload. Preload must be CJS, not ESM. Use the Vite preload config to target CommonJS.- macOS app crashes immediately after notarization. Entitlements missing for what your app does. Check Console.app on macOS for the crash reason.
- App icon doesn’t appear. Icon path in
packagerConfig.iconis a base path without extension. Forge picks the right extension per platform:icon.icnsfor macOS,icon.icofor Windows. - Squirrel installer doesn’t update. Squirrel uses delta updates; sometimes installs cleanly fail. Test fresh-install and update separately.
process.env.NODE_ENVis “production” but you’re in dev. Forge setsNODE_ENV=productionduring build. Detect dev withapp.isPackagedinstead.- Logs not showing.
console.login main goes to terminal duringnpm start, but to~/Library/Logs/<app>(macOS) or%APPDATA%\<app>\logs(Windows) in packaged builds. Useelectron-logfor consistent file logging. asarcan’t find a file at runtime. Files referenced by path (templates, fonts) inside asar needapp.getAppPath()resolution. For binaries that must be on-disk, add them toextraResource.- Notarized DMG won’t open on Apple Silicon despite x64 build. Universal binaries require building both arch slices and merging with
lipo. Forge supports--arch=universalfor this. - Squirrel rejects updates with version mismatch. Windows Squirrel parses semver strictly; pre-release tags like
1.0.0-rc.1break it. Stick to plainX.Y.Zfor shipped builds.
For related desktop / packaging issues, see Tauri not working, Electron not working, Electron require is not defined, and React Native Metro bundler failed.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: python: command not found (or python3: No such file or directory)
How to fix 'python: command not found', 'python3: command not found', and wrong Python version errors on Linux, macOS, Windows, and Docker. Covers PATH, symlinks, pyenv, update-alternatives, Homebrew, and more.
Fix: ERROR: Could not build wheels / Failed building wheel (pip)
How to fix pip 'ERROR: Could not build wheels', 'Failed building wheel', 'No matching distribution found', and 'error: subprocess-exited-with-error'. Covers missing C compilers, build tools, system libraries, Python version issues, pre-built wheels, and platform-specific fixes for Linux, macOS, and Windows.
Fix: Tauri 2 Not Working — Capabilities, Plugin Permissions, Mobile Build, and v1 Migration
How to fix Tauri 2 errors — invoke command not allowed by capabilities, plugin permission missing, tauri.conf.json schema, mobile init/build failures, updater migration, and v1 allowlist conversion.
Fix: Electron Not Working — Window Not Showing, IPC Not Communicating, or Build Failing
How to fix Electron issues — main and renderer process setup, IPC communication with contextBridge, preload scripts, auto-update, native module rebuilding, and packaging with electron-builder.