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-updaterpackage.json: Set
"main": "main.js"and add build scripts. Examplepackage.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 requireelectron-updater. Only check for updates in production (whenapp.isPackagedis 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
publishsection 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 useprovider: "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:
Generate a GitHub token: Create a Personal Access Token (PAT) at github.com/settings/tokens with repo scope.
Store the token: In CI (e.g., GitHub Actions), save it as a secret (e.g.,
GH_TOKENorGITHUB_TOKEN).Set environment: In your CI pipeline, set
GH_TOKEN. Electron-builder uses this to publish. IfGH_TOKEN/GITHUB_TOKENis defined, it defaults to using GitHub. For example, in GitHub Actions you can use the built-insecrets.GITHUB_TOKEN.Build and publish: Run
electron-builderwith 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"inpublishconfig and pointurlto 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-*.ymlfiles to that URL manually or via a script/CI.In your app’s code, set the feed URL to that location (using
autoUpdater.setFeedURLor by instantiating aGenericupdater).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": trueunder"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:
App starts and calls
checkForUpdates...().On
'update-available', you might show a notification or dialog.As the update downloads, the
'download-progress'event fires – you can display a progress bar.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(); } });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
.exeinstaller. The update metadatalatest.ymland 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
.appinside a.dmgand also a.zip. The ZIP is used by the updater (Squirrel-like). Must be signed and notarized for updates to work. Thelatest-mac.ymlfile 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.ymland*.AppImageare used. Note: on Linux, users often rely on AppImage or third-party update servers.Packaging differences: Make sure
build.targetis set appropriately (e.g.nsisfor Windows,dmg/zipfor macOS,AppImagefor 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 yourpublishsettings) in the project root and setautoUpdater.forceDevUpdateConfig = true. This makes the updater read your dev YAML as if it werelatest.yml.Production test: Publish a new release on GitHub (or your server). Run the currently installed app; it should detect the new
latest.ymland 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-updaterallows downgrades (allowDowngrade: truewhen channels are used), preventing issues if a new version is broken.Logging: Use
autoUpdater.logger(likeelectron-log) to capture update errors and info. Check logs if users report update failures.Versioning: Follow Semantic Versioning. Use
generateUpdatesFilesForAllChannelsand 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):
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.




