Skip to content

Fix: Electron Forge Not Working — Makers, Code Signing, Native Modules, and Publishers

FixDevs ·

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, make produces 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, make produces 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 Windows

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

  1. Apple Developer account ($99/year).
  2. Developer ID Application certificate (download from Apple Developer → Certificates).
  3. App-specific password for notarization (appleid.apple.com → Sign-In and Security → App-Specific Passwords).
  4. 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 make

Notarization 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-rebuild

Forge 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 publish

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

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

  • make succeeds but installer crashes. Test the unpacked app first: npm run package then run from out/. If it works, the issue is in the installer creation, not the app code.
  • Native module not found at runtime. Add to packagerConfig.extraResource or use auto-unpack-natives.
  • SyntaxError: Unexpected token in 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.icon is a base path without extension. Forge picks the right extension per platform: icon.icns for macOS, icon.ico for Windows.
  • Squirrel installer doesn’t update. Squirrel uses delta updates; sometimes installs cleanly fail. Test fresh-install and update separately.
  • process.env.NODE_ENV is “production” but you’re in dev. Forge sets NODE_ENV=production during build. Detect dev with app.isPackaged instead.
  • Logs not showing. console.log in main goes to terminal during npm start, but to ~/Library/Logs/<app> (macOS) or %APPDATA%\<app>\logs (Windows) in packaged builds. Use electron-log for 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles