Skip to main content

Command Palette

Search for a command to run...

Implementing Auto-Updates in Electron with electron-updater

Updated
11 min read
Implementing Auto-Updates in Electron with electron-updater

Automatic updates let your Electron app fetch and install new releases without user intervention. This guide walks through adding auto-update using the electron-updater package (part of [electron-builder]). It covers setting up the app, configuring electron-builder for Windows/macOS/Linux, hosting updates on GitHub (and alternatives like Gitea/GitBucket or a custom server), using update channels (alpha/beta), handling update events and UI, and setting up CI/CD.

Electron-builder’s auto-update supports DMG (macOS), AppImage/DEB/Pacman/RPM (Linux), and NSIS (Windows) by default. (Squirrel.Windows is not supported on Windows.) On macOS, apps must be code-signed for updates to work.

1. Setting Up a Basic Electron App

  • Initialize the project: npm init -y. Install Electron and electron-builder:

      npm install --save-dev electron electron-builder
      npm install --save electron-updater
    
  • package.json: Set "main": "main.js" and add build scripts. Example package.json:

      {
        "name": "my-electron-app",
        "version": "1.0.0",
        "main": "main.js",
        "scripts": {
          "start": "electron .",
          "dist": "electron-builder"
        },
        "devDependencies": {
          "electron": "^xx.xx.x",
          "electron-builder": "^xx.xx.x"
        },
        "dependencies": {
          "electron-updater": "^yy.yy.y"
        }
      }
    
  • Main process code: In main.js, create the BrowserWindow and require electron-updater. Only check for updates in production (when app.isPackaged is true) to avoid network calls during development.

2. Integrating electron-updater

Install electron-updater as above. In the main process, import and use it:

const { app, BrowserWindow } = require('electron');
const { autoUpdater } = require('electron-updater');
const log = require('electron-log');

log.transports.file.level = 'info';
autoUpdater.logger = log;  // Log update events

app.on('ready', () => {
  createWindow();
  if (app.isPackaged) {
    // Check for updates on launch
    autoUpdater.checkForUpdatesAndNotify();
  }
});

By default, autoUpdater.checkForUpdatesAndNotify() will download updates and show a notification. Handle events to customize behavior:

autoUpdater.on('checking-for-update', () => {
  log.info('Checking for update...');
});
autoUpdater.on('update-available', info => {
  log.info('Update available:', info.version);
});
autoUpdater.on('update-not-available', () => {
  log.info('No updates found.');
});
autoUpdater.on('download-progress', progress => {
  // e.g. send progress to renderer for a progress bar
  log.info(`Download speed: ${progress.bytesPerSecond}`);
});
autoUpdater.on('update-downloaded', info => {
  log.info('Update downloaded:', info.version);
  // Optionally prompt user and install
  autoUpdater.quitAndInstall();
});
autoUpdater.on('error', err => {
  log.error('Update error:', err);
});

Calling autoUpdater.quitAndInstall() will restart and install the update. It should only be called after update-downloaded. Note: even if you don’t call it, the update will apply on next app launch.

3. Configuring electron-builder for Windows/macOS/Linux

In package.json (or an electron-builder.yml), add a build section. For example:

"build": {
  "appId": "com.example.myapp",
  "productName": "MyApp",
  "files": ["**/*"],
  "directories": { "buildResources": "resources" },
  "mac": {
    "target": ["dmg", "zip"]
  },
  "win": {
    "target": "nsis"
  },
  "linux": {
    "target": ["AppImage", "deb"]
  },
  "publish": [
    {
      "provider": "github",
      "owner": "your-github-username",
      "repo": "your-repo-name"
    }
  ]
}
  • Targets: By default, macOS builds a DMG (+ ZIP), Windows builds an NSIS installer, and Linux builds AppImage/DEB/etc. These metadata formats (latest.yml, latest-mac.yml) are required for auto-updates.

  • Signing: Remember code-signing for macOS (certificate must be installed in CI or locally).

  • Publish config: The publish section tells electron-builder where to upload build artifacts and update metadata. In this example, it uses GitHub Releases. For a generic HTTP server (or Gitea/GitBucket), you would use provider: "generic" with a URL.

4. Using GitHub Releases for Updates

GitHub Releases is the simplest update host. With publish.provider = "github", electron-builder will create a GitHub Release and upload your installers along with a latest.yml. To use it:

  1. Generate a GitHub token: Create a Personal Access Token (PAT) at github.com/settings/tokens with repo scope.

  2. Store the token: In CI (e.g., GitHub Actions), save it as a secret (e.g., GH_TOKEN or GITHUB_TOKEN).

  3. Set environment: In your CI pipeline, set GH_TOKEN. Electron-builder uses this to publish. If GH_TOKEN/GITHUB_TOKEN is defined, it defaults to using GitHub. For example, in GitHub Actions you can use the built-in secrets.GITHUB_TOKEN.

  4. Build and publish: Run electron-builder with publishing. For instance, in GitHub Actions:

     - name: Build and Publish
       run: npx electron-builder --publish always
       env:
         GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    

    This will create a draft release (on the pushed tag) and attach the artifacts. You can also use the action-electron-builder to simplify setup.

When using channels like -beta, you might set releaseType or EP_PRE_RELEASE=true, but it’s often easier to just tag your release accordingly or set private: false/true in config for public/private repos. If you need private update repos, electron-updater supports it by using the GitHub API when GH_TOKEN and "private": true are set. However, note the GitHub API rate limit (~5000 requests per hour) (each update check uses multiple requests), so private repos should be used only when necessary.

5. Using GitBucket/Gitea (Self-Hosted Git)

GitBucket, Gitea, GitLab, and similar self-hosted Git services don’t have built-in providers in electron-builder. Typically you:

  • Use provider: "generic" in publish config and point url to your server’s download directory. For example:

      "publish": [
        {
          "provider": "generic",
          "url": "https://git.example.com/owner/repo/releases",
          "channel": "beta"
        }
      ]
    
  • Upload the built files and the latest.yml/latest-*.yml files to that URL manually or via a script/CI.

  • In your app’s code, set the feed URL to that location (using autoUpdater.setFeedURL or by instantiating a Generic updater).

  • If authentication is required on the server, use request headers (see next section).

Essentially, treat the self-hosted Git like a static file server for updates. Electron-updater itself won’t automatically use the GitBucket/Gitea API, so the generic approach is used.

6. Custom Generic Update Server (Node/Express Example)

You can roll your own update server. For example, with Node/Express:

const express = require('express');
const app = express();
const path = require('path');

// Simple auth middleware:
app.use((req, res, next) => {
  const token = req.headers.authorization;
  if (token === 'Bearer mySecureToken') {
    next();
  } else {
    res.sendStatus(401);
  }
});

// Serve metadata and files
app.get('/update/latest.yml', (req, res) => {
  res.sendFile(path.join(__dirname, 'dist', 'latest.yml'));
});
app.get('/update/:file', (req, res) => {
  res.sendFile(path.join(__dirname, 'dist', req.params.file));
});

app.listen(3000, () => console.log('Update server running'));

On the client side, configure the updater to use this server:

autoUpdater.requestHeaders = { 'Authorization': 'Bearer mySecureToken' };
autoUpdater.setFeedURL({
  provider: 'generic',
  url: 'https://myserver.com/update'
});
autoUpdater.checkForUpdatesAndNotify();

This tells electron-updater to fetch latest.yml and installers from your API. The auth token is sent via headers. If you instantiate a platform-specific updater (e.g. new NsisUpdater(options)), you can also use autoUpdater.addAuthHeader('Bearer ...').

With a custom server, you have full control (logging, rate-limiting, custom authentication) but must handle SSL certificates and tokens yourself.

7. Using Request Headers with autoUpdater

For secure endpoints, set autoUpdater.requestHeaders before checking for updates:

autoUpdater.requestHeaders = {
  'Authorization': 'Bearer <ACCESS_TOKEN>'
};
autoUpdater.checkForUpdatesAndNotify();

Alternatively, if using a custom updater instance (like NsisUpdater), you can use addAuthHeader() as shown above. In your electron-builder publish config, you can also specify a requestHeaders field which will be included in the latest.yml (valid in newer versions). Always load tokens from environment/secure storage, never hard-code them.

8. Update Channels (beta, alpha, stable)

Electron-builder lets you create release channels via version tags. To publish beta/alpha:

  • In package.json, set "version": "1.2.3-beta" and add
    "generateUpdatesFilesForAllChannels": true under "build".
    This ensures metadata (beta, alpha, latest) are generated.

  • In your app, set the channel before checking updates:

      autoUpdater.channel = 'beta';
      autoUpdater.checkForUpdatesAndNotify();
    

    Users on the beta channel will receive both beta and latest updates. Users on alpha get alpha→beta→latest. Those on latest (no suffix) get only stable releases.

For GitHub Releases, you should also mark pre-releases accordingly or use releaseType: "draft"/"prerelease" in electron-builder. Note that electron-builder may ignore -beta suffix unless generateUpdatesFilesForAllChannels is true, so explicit channel config is recommended.

9. Handling Update Flow and UI

Typical flow:

  1. App starts and calls checkForUpdates...().

  2. On 'update-available', you might show a notification or dialog.

  3. As the update downloads, the 'download-progress' event fires – you can display a progress bar.

  4. When 'update-downloaded' fires, prompt the user to install. Example (in main process):

     autoUpdater.on('update-downloaded', () => {
       const { dialog } = require('electron');
       const idx = dialog.showMessageBoxSync({
         type: 'question', buttons: ['Restart', 'Later'],
         message: 'A new version is ready. Restart now to install?'
       });
       if (idx === 0) {
         autoUpdater.quitAndInstall();
       }
     });
    
  5. Listen for 'error' to catch download failures and inform the user.

By default, checkForUpdatesAndNotify() uses system notifications. For a fully custom UI, relay events to your renderer (via ipcMain) and design dialogs. Always log progress for debugging (e.g., use electron-log as shown above).

10. Platform-Specific Packaging Behaviors

  • Windows (NSIS): Produces a single .exe installer. The update metadata latest.yml and the installer (.exe) are published. NSIS supports delta updates if enabled (out of scope here). Electron-builder will use the NSIS installer for auto-updates on Windows.

  • macOS (DMG/ZIP): Builds a signed .app inside a .dmg and also a .zip. The ZIP is used by the updater (Squirrel-like). Must be signed and notarized for updates to work. The latest-mac.yml file is generated automatically.

  • Linux (AppImage/DEB): Builds an AppImage by default. You can host a repository for DEB/RPM or use AppImage updates on GitHub. The latest.yml and *.AppImage are used. Note: on Linux, users often rely on AppImage or third-party update servers.

  • Packaging differences: Make sure build.target is set appropriately (e.g. nsis for Windows, dmg/zip for macOS, AppImage for Linux). Electron-builder takes care of creating the correct update metadata per platform.

11. CI/CD Automation (e.g. GitHub Actions)

Automate builds, signing, and publishing so updates happen on every release:

name: Build & Release
on:
  push:
    tags:
      - 'v*.*.*'
jobs:
  release:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
    steps:
      - uses: actions/checkout@v2
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: 18
      - name: Install dependencies
        run: npm ci
      - name: Build and Publish
        run: npx electron-builder --publish always
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

This workflow triggers on tags like v1.2.3, runs on all OSes, and uses electron-builder --publish always to create GitHub Releases. For Windows/macOS code signing, configure CSC_LINK, CSC_KEY_PASSWORD (for Windows cert) and APPLE_ID, APPLE_PASSWORD (for macOS notarization) as secrets. In GitLab CI or others, the process is similar. Once set up, pushing a version tag will automatically produce installers and update metadata, so your app’s auto-update can work continuously.

12. Testing Auto-Updates

  • Local testing: You can simulate an update by running a local HTTP/S3 server. Electron-builder suggests using MinIO (an S3-compatible server) for local testing. For example, serve the dist/ folder via MinIO or even a simple Express/NGINX.

  • Dev mode: To test without packaging, create a dev-app-update.yml (matching your publish settings) in the project root and set autoUpdater.forceDevUpdateConfig = true. This makes the updater read your dev YAML as if it were latest.yml.

  • Production test: Publish a new release on GitHub (or your server). Run the currently installed app; it should detect the new latest.yml and download the update.

Always bump the app’s version (in package.json) for each release and commit that change so updates are recognized. Use different channels for testing (e.g. release a -beta build first).

13. Best Practices for UX and Reliability

  • Informative UI: Always let the user know an update is happening. Show progress and let them choose when to restart (unless it’s critical).

  • Rollback safety: By default electron-updater allows downgrades (allowDowngrade: true when channels are used), preventing issues if a new version is broken.

  • Logging: Use autoUpdater.logger (like electron-log) to capture update errors and info. Check logs if users report update failures.

  • Versioning: Follow Semantic Versioning. Use generateUpdatesFilesForAllChannels and suffixes for non-stable releases.

  • Security: Always use HTTPS and authenticate update requests. Do not expose tokens. For GitHub, use fine-grained tokens with minimum scopes.

  • Fail gracefully: Handle autoUpdater.on('error') to retry or notify user if something goes wrong.

14. Comparison of Update Hosting Options

AspectGitHub ReleasesGitBucket/Gitea (Self-Hosted)Custom Update Server
ProviderBuilt-in support (electron-builder)Use generic providerFully custom (HTTP/S3)
Setup DifficultyEasiest (auto via builder)Moderate (install & configure)Hard (build+secure server)
AuthenticationGH_TOKEN with repo scope, supports private via APINone by default (or custom)Custom (e.g. tokens/headers)
IntegrationAutomatic GitHub ReleasesManual upload of assetsManual or scripted uploads
Rate Limits5000 req/hr (GitHub API)Limited by your serverOnly your server/bandwidth
SecurityGitHub ACLs, HTTPSYour responsibilityYour responsibility
Bandwidth/CostGitHub provides CDNDepends on your infraYou pay for hosting

15. Code Snippets & Examples

Folder structure (example):

MyElectronApp/
├─ package.json
├─ main.js
├─ renderer.js
├─ resources/
│   └─ icon.png
├─ .github/
│   └─ workflows/
│       └─ build.yml
└─ build/
    └─ MyApp-icon.icns

package.json (excerpt with build config):

{
  "name": "my-electron-app",
  "version": "1.2.3",
  "build": {
    "appId": "com.example.app",
    "publish": [
      { "provider": "github", "owner": "user", "repo": "my-electron-app" }
    ],
    "mac": { "target": ["dmg", "zip"] },
    "win": { "target": "nsis" },
    "linux": { "target": ["AppImage"] }
  }
}

GitHub Actions workflow (build.yml):

name: CI
on:
  push:
    tags: 
      - 'v*.*.*'
jobs:
  release:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix: { os: [ubuntu-latest, macos-latest, windows-latest] }
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v3
        with: { node-version: '18' }
      - run: npm ci
      - run: npx electron-builder --publish always
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Electron code (main.js excerpt):

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

app.whenReady().then(() => {
  // create windows...
  if (app.isPackaged) {
    autoUpdater.checkForUpdatesAndNotify();
  }
});

Express update server (server.js):

const express = require('express');
const app = express();
const path = require('path');

// Simple auth check
app.use((req, res, next) => {
  if (req.headers.authorization === 'Bearer mySecureToken') next();
  else res.sendStatus(401);
});

// Serve metadata
app.get('/update/latest.yml', (req, res) => {
  res.sendFile(path.join(__dirname, 'dist', 'latest.yml'));
});
// Serve update files
app.get('/update/:file', (req, res) => {
  res.sendFile(path.join(__dirname, 'dist', req.params.file));
});

app.listen(3000, () => console.log('Update server running'));

By following the above steps and configurations, your Electron app will automatically download and install updates across Windows, macOS, and Linux. Ensure you sign macOS builds, manage your versioning carefully, and test updates both locally and in staging before wide release. With CI/CD pipelines and electron-updater set up properly, users will get a seamless, reliable update experience.

Sources: Electron Builder documentation and Electron official docs.