Cover image for Electron App Revolution: Enhance with Automatic Updates

Sahil Khurana

What Electron Is

Electron packages Chromium with Node.js into a single runtime. You write your app in HTML, CSS, and JavaScript and ship it as a native desktop executable for Windows, macOS, and Linux. VS Code, Slack, Figma's desktop client — all Electron.


The Three Core Files

my-electron-app/
├── index.html    ← the UI rendered in the app window
├── main.js       ← Node.js main process — window management, app lifecycle
└── preload.js    ← bridge — safely exposes Node APIs to the renderer

Enter fullscreen mode Exit fullscreen mode

Quick setup:

mkdir my-electron-app && cd my-electron-app
npm init -y
npm install --save-dev electron

Enter fullscreen mode Exit fullscreen mode

Add to package.json:

{ "scripts": { "start": "electron ." } }

Enter fullscreen mode Exit fullscreen mode

Minimal main.js:

const { app, BrowserWindow } = require('electron');
const path = require('path');

function createWindow() {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
    },
  });
  win.loadFile('index.html');
}

app.whenReady().then(() => {
  createWindow();
  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) createWindow();
  });
});

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit();
});

Enter fullscreen mode Exit fullscreen mode

Run npm start — window opens, app is running. That's the foundation everything else builds on.


Main vs. Renderer Process

Main process — one per app, runs in Node.js. Creates windows, handles OS APIs (file system, tray, notifications), manages app lifecycle.

Renderer process — one per window, runs in Chromium. This is where your HTML and frontend JS execute. Sandboxed from Node.js by default.

IPC — how they talk to each other. ipcMain and ipcRenderer pass messages between processes. You'll need this for wiring the update UI.

// main.js — listen for renderer messages
const { ipcMain } = require('electron');
ipcMain.on('check-for-updates', () => autoUpdater.checkForUpdates());

// renderer.js — send message to main
const { ipcRenderer } = require('electron');
document.getElementById('update-btn').addEventListener('click', () => {
  ipcRenderer.send('check-for-updates');
});

Enter fullscreen mode Exit fullscreen mode


Packaging

Before distribution, the app needs to be packaged into platform installers. Two tools dominate:

  • electron-forge — officially recommended, simpler setup, handles build + package + publish in one workflow
  • electron-builder — more configurable, better for complex signing/distribution requirements

For new projects, start with electron-forge unless you have a specific reason not to.


Code Signing

Both macOS and Windows require code signing. Skip it and users get security warnings — on macOS the app can be blocked outright.

macOS — sign at packaging time with an Apple Developer certificate. Notarization (Apple's security scan) is also required since Catalina.

Windows — the distributable installer is signed. Get a certificate from DigiCert, Sectigo, or similar and configure it in your build settings.

Both tools read signing credentials from environment variables. Keep them out of your codebase.


Auto-Update Setup

Electron's auto-update is free and built in. Four requirements:

  1. App runs on macOS or Windows
  2. GitHub repo exists (public = no extra config; private = set GH_TOKEN)
  3. Builds published to GitHub Releases
  4. Builds are code signed

With electron-forge

npm install --save-dev @electron-forge/publisher-github

Enter fullscreen mode Exit fullscreen mode

forge.config.js:

module.exports = {
  publishers: [
    {
      name: '@electron-forge/publisher-github',
      config: {
        repository: {
          owner: 'your-github-username',
          name: 'your-repo-name',
        },
        prerelease: false,
      },
    },
  ],
};

Enter fullscreen mode Exit fullscreen mode

package.json script:

{ "scripts": { "publish": "electron-forge publish" } }

Enter fullscreen mode Exit fullscreen mode

With electron-builder

npm install --save-dev electron-builder

Enter fullscreen mode Exit fullscreen mode

package.json:

{
  "build": {
    "appId": "com.yourcompany.yourapp",
    "publish": {
      "provider": "github",
      "owner": "your-github-username",
      "repo": "your-repo-name"
    }
  },
  "scripts": {
    "dist": "electron-builder",
    "release": "electron-builder --publish always"
  }
}

Enter fullscreen mode Exit fullscreen mode

Wiring Update Events in the App

const { autoUpdater } = require('electron-updater');

app.whenReady().then(() => {
  autoUpdater.checkForUpdatesAndNotify();
});

// Tell the renderer when an update is ready
autoUpdater.on('update-downloaded', (info) => {
  mainWindow.webContents.send('update-available', info.version);
});

// Install when the user confirms
ipcMain.on('install-update', () => {
  autoUpdater.quitAndInstall();
});

Enter fullscreen mode Exit fullscreen mode

Renderer side:

ipcRenderer.on('update-available', (event, version) => {
  const userConfirmed = confirm(`Version ${version} is ready. Restart to install?`);
  if (userConfirmed) ipcRenderer.send('install-update');
});

Enter fullscreen mode Exit fullscreen mode

Always prompt — never force-quit mid-session.


GitHub Actions CI

Automate releases on every version tag:

name: Release

on:
  push:
    tags:
      - 'v*'

jobs:
  release:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [macos-latest, windows-latest]

    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 18
      - run: npm install
      - name: Publish
        env:
          GH_TOKEN: ${{ secrets.GH_TOKEN }}
          CSC_LINK: ${{ secrets.CSC_LINK }}
          CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
        run: npm run release

Enter fullscreen mode Exit fullscreen mode

Tag a release (git tag v1.2.0 && git push --tags), the workflow fires, builds for both platforms, signs, and creates the GitHub Release. Running instances auto-update on the next check.


Things Worth Knowing Before You Ship

Full vs. delta updates — by default, the full installer downloads on each update. For large apps, configure delta updates to reduce download size.

Update channels — use GitHub pre-releases + electron-updater's channel option to run a beta channel before pushing to all users.

macOS notarization — required since Catalina, adds a few minutes to CI, needs Apple Developer account setup. Plan for it upfront.

Don't force-restartquitAndInstall() is immediate. Always give users a chance to save their work first.


Building Desktop Apps with Electron?

At Innostax, we build with both — the choice comes from what the project actually needs, not what we happen to prefer. Whether electron-forge or electron-builder fits better depends on what the distribution requirements actually demand.

If you're figuring out the right stack for something you're building, innostax.com/contact is the right place to start that conversation.

Originally published on the Innostax Engineering Blog.