# 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:
    
    ```bash
    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`:
    
    ```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:

```js
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:

```js
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:

```json
"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](https://github.com/settings/tokens/new) 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:
    
    ```yaml
    - 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](https://github.com/samuelmeuli/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:
    
    ```json
    "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:

```js
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:

```js
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:

```js
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:
    
    ```js
    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):
    
    ```js
    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](https://github.com/megahertz/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:

```yaml
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](https://min.io/) (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

| Aspect | GitHub Releases | GitBucket/Gitea (Self-Hosted) | Custom Update Server |
| --- | --- | --- | --- |
| **Provider** | Built-in support (electron-builder) | Use *generic* provider | Fully custom (HTTP/S3) |
| **Setup Difficulty** | Easiest (auto via builder) | Moderate (install & configure) | Hard (build+secure server) |
| **Authentication** | GH\_TOKEN with `repo` scope, supports private via API | None by default (or custom) | Custom (e.g. tokens/headers) |
| **Integration** | Automatic GitHub Releases | Manual upload of assets | Manual or scripted uploads |
| **Rate Limits** | 5000 req/hr (GitHub API) | Limited by your server | Only your server/bandwidth |
| **Security** | GitHub ACLs, HTTPS | Your responsibility | Your responsibility |
| **Bandwidth/Cost** | GitHub provides CDN | Depends on your infra | You pay for hosting |

## 15\. Code Snippets & Examples

**Folder structure (example):**

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

**package.json** (excerpt with build config):

```json
{
  "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`):

```yaml
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):

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

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

**Express update server** (`server.js`):

```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.
