
Auto-Update for Desktop Apps: Electron Forge Guide
Distributing a desktop app without an auto-update mechanism is signing a contract with future chaos. In six months, you'll have users on five different versions of the app, each with different bugs, different behaviors, and requesting support for problems that were fixed weeks ago. Tracking which version each customer is running and convincing them to update manually is work that shouldn't exist.
Well-implemented auto-update eliminates this problem at the root. The user installs once. From then on, the app stays updated silently in the background -- or with a discreet notification when an update is available. The user base converges to the latest version in days, not months.
The good news is that the Electron ecosystem has a mature solution for this: electron-updater, which works with GitHub Releases as a distribution server. The bad news is that implementing it correctly -- with release channels, gradual rollout, and a rollback strategy -- takes more care than the basic documentation example suggests.
electron-updater: Basic Setup with GitHub Releases
electron-updater is part of the electron-builder package and is the most widely used solution for auto-update in Electron apps. It periodically checks whether a new version is available on an update server -- which can be GitHub Releases, S3, or a custom server.
The initial setup requires configuration in package.json and the main process:
// main.js — auto-updater configuration
const { autoUpdater } = require('electron-updater')
const { ipcMain, dialog } = require('electron')
const log = require('electron-log')
// Configure logging for diagnostics
autoUpdater.logger = log
autoUpdater.logger.transports.file.level = 'info'
// Disable auto-download to control when to install
autoUpdater.autoDownload = false
function setupAutoUpdater(mainWindow) {
// Check for updates on startup (with 3 second delay)
setTimeout(() => {
autoUpdater.checkForUpdates()
}, 3000)
// Check again every 4 hours
setInterval(() => {
autoUpdater.checkForUpdates()
}, 4 * 60 * 60 * 1000)
autoUpdater.on('update-available', (info) => {
// Notify the renderer process
mainWindow.webContents.send('update-available', info)
})
autoUpdater.on('download-progress', (progress) => {
mainWindow.webContents.send('download-progress', progress)
})
autoUpdater.on('update-downloaded', (info) => {
mainWindow.webContents.send('update-downloaded', info)
})
// Start download when user confirms
ipcMain.on('start-download', () => {
autoUpdater.downloadUpdate()
})
// Install and restart when user confirms
ipcMain.on('install-update', () => {
autoUpdater.quitAndInstall(false, true)
})
}
module.exports = { setupAutoUpdater }
The package.json configuration defines the publish server:
{
"build": {
"publish": {
"provider": "github",
"owner": "your-username",
"repo": "your-repository",
"private": false
}
}
}
With this configuration, when you publish a GitHub Release with the build artifacts, electron-updater automatically detects the new version and starts the update process.
Release Channels: Stable, Beta, and Nightly
A single release channel isn't enough for most serious projects. The industry standard practice is to have at least two channels: stable for the production user base, and beta for testers who accept receiving unfinished versions in exchange for early access to new features.
electron-updater supports channels natively. The version in package.json defines the channel:
1.0.0-- stable version1.1.0-beta.1-- beta version1.1.0-alpha.1-- alpha/nightly version
In code, the channel can be configured dynamically based on a user preference:
// Read channel preference from saved settings
const channel = store.get('updateChannel', 'stable')
autoUpdater.channel = channel
// In the settings UI, allow the user to switch channels
ipcMain.on('set-update-channel', (event, newChannel) => {
store.set('updateChannel', newChannel)
autoUpdater.channel = newChannel
autoUpdater.checkForUpdates()
})
Communicating the channel to the right audience matters: users who opted into the beta channel know they may receive versions with occasional bugs. This segments feedback and lets you identify issues before they affect the entire user base.
Staged Updates: Gradual Rollout by Percentage
For applications with a large user base, releasing a version to everyone simultaneously is risky. A critical bug that slips through testing will affect all users at the same time.
Gradual rollout -- releasing to 5% of users first, observing metrics for 24 hours, then expanding to 20%, 50%, and 100% -- is a standard practice to mitigate this risk.
electron-updater doesn't have native support for percentage-based rollout, but it can be implemented with server-side logic or a feature flag layer:
// Strategy: check rollout flag before starting update
autoUpdater.on('update-available', async (info) => {
// Generate a stable unique identifier for this instance
const instanceId = getOrCreateInstanceId()
// Query server to check if this instance is in the rollout
try {
const response = await fetch(
`https://api.yourapp.com/update-eligibility?version=${info.version}&instanceId=${instanceId}`
)
const { eligible } = await response.json()
if (eligible) {
mainWindow.webContents.send('update-available', info)
}
} catch {
// On verification failure, don't block the update
mainWindow.webContents.send('update-available', info)
}
})
The server controls the percentage of eligible instances for each version, allowing you to gradually increase the rollout as confidence in the version grows.
Rollback: How to Revert a Problematic Version
No QA process is perfect. Eventually a problematic version will reach users. Having a rollback mechanism -- whether automatic or manual -- is the difference between a controlled incident and a support crisis.
The simplest strategy is to keep the previous version available on GitHub Releases and redirect the update server to it when necessary. If latest.yml (the file electron-updater checks) points to the previous version, instances that haven't updated yet won't receive the problematic version.
For users who already installed the buggy version, the most effective approach is to publish a new corrected version -- even if it's a minimal hotfix -- as quickly as possible. The auto-update then replaces the problematic version with the fixed one.
In critical cases, where the app is crashing on startup and auto-update can't execute, you need a documented manual procedure. This includes keeping installers for previous versions available for direct download and having a communication channel with affected users.
The most important point about rollback is: plan before you need it. When there's a production incident is not the time to discover that the rollback process was never tested.
Conclusion
Well-implemented auto-update is not a distribution detail -- it's part of the product experience. Users who never need to worry about manually updating the app have less friction in their daily workflow. Development teams that can ship updates with confidence, knowing they have staging channels and rollback capability, build with more agility.
The real complexity is in the details: signing installers correctly (without code signing, Windows blocks updates), handling users who leave their computer running without restarting, and communicating critical security updates urgently but without being intrusive.
At SystemForge, auto-update with channels, staged rollout, and rollback capability is part of the baseline for every desktop app we build. If you're developing an Electron app and want to implement a robust update strategy from the start, let's talk.
Need Desktop Software?
We build cross-platform desktop applications with Electron or Tauri.
Learn more →Need help?
