Fix: Electron Forge Not Working — Makers, Code Signing, Native Modules, and Publishers
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). Most issues map to:
- 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.
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.
For related desktop / packaging issues, see Tauri not working, Electron not working, Electron require is not defined, and Vite failed to resolve import.
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.