Electron Auto-Update Patterns
Quick Guide: Use
electron-updater(from electron-builder) for cross-platform auto-updates. It supports macOS (DMG), Windows (NSIS), and Linux (AppImage/DEB/RPM). Configure a provider (GitHub, S3, generic server) in yourelectron-builderconfig. The updater emits lifecycle events:checking-for-update->update-available->download-progress->update-downloaded. SetautoDownload: falsefor manual download control. Use channels (latest/beta/alpha) for staged releases andstagingPercentagefor gradual rollouts. Code signing is mandatory on macOS and strongly recommended on Windows.
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST guard update checks with app.isPackaged -- calling checkForUpdates() in development causes confusing errors and network calls to non-existent endpoints)
(You MUST handle the error event on the updater -- unhandled update errors crash the main process)
(You MUST code-sign macOS builds -- unsigned apps cannot auto-update and the updater silently fails)
(You MUST NOT call quitAndInstall() without confirming the user's intent -- forcing a restart mid-work causes data loss)
(You MUST use named constants for all intervals and timeouts -- no magic numbers in setInterval or retry logic)
Auto-detection: electron-updater, autoUpdater from electron-updater, checkForUpdates, checkForUpdatesAndNotify, update-available, update-downloaded, download-progress, quitAndInstall, autoDownload, stagingPercentage, dev-app-update.yml, NsisUpdater, MacUpdater, AppImageUpdater, setFeedURL, allowPrerelease, allowDowngrade, forceDevUpdateConfig, disableDifferentialDownload
When to use:
- Implementing auto-updates in Electron apps built with electron-builder
- Configuring update providers (GitHub Releases, S3, generic HTTP server)
- Setting up update channels for beta/alpha testing
- Implementing staged rollouts with percentage-based distribution
- Controlling download behavior (manual download, progress tracking)
- Handling update errors with retry strategies
- Testing the update flow locally during development
When NOT to use:
- Apps packaged with Electron Forge using Squirrel (use Electron's built-in
autoUpdatermodule instead) - Apps distributed exclusively through platform app stores (macOS App Store, Microsoft Store) -- those have their own update mechanisms
- Apps that only need to check for updates and show a "download from website" link (no in-app update needed)
Key Patterns
Pattern 1: Basic Setup with Lifecycle Events
Import autoUpdater from electron-updater (not Electron's built-in module). Wire up lifecycle events in the main process after the app is ready.
import { autoUpdater } from "electron-updater";
const CHECK_INTERVAL_MS = 4 * 60 * 60 * 1000; // 4 hours
function setupAutoUpdater(mainWindow) {
if (!app.isPackaged) return; // Never check in development
autoUpdater.on("update-available", (info) => {
mainWindow.webContents.send("update-available", info);
});
autoUpdater.on("update-downloaded", (info) => {
mainWindow.webContents.send("update-downloaded", info);
});
autoUpdater.on("error", (error) => {
log.error("Update error:", error);
});
autoUpdater.checkForUpdatesAndNotify();
setInterval(() => autoUpdater.checkForUpdates(), CHECK_INTERVAL_MS);
}
Key point: checkForUpdatesAndNotify() checks and shows a native OS notification when an update downloads. Use checkForUpdates() for silent checks when you handle UI yourself. See examples/core.md.
Pattern 2: Manual Download Control
Set autoDownload: false to let users decide when to download. This is essential for metered connections or large updates.
autoUpdater.autoDownload = false;
autoUpdater.on("update-available", (info) => {
// Show UI prompt -- user decides whether to download
mainWindow.webContents.send("update-available", info);
});
// User clicks "Download" in the renderer
ipcMain.handle("start-update-download", () => {
return autoUpdater.downloadUpdate();
});
Key point: With autoDownload: false, the download-progress and update-downloaded events only fire after you explicitly call downloadUpdate(). See examples/core.md.
Pattern 3: Update Providers
Configure where the updater looks for releases. The provider is set in your electron-builder config file and can be overridden at runtime with setFeedURL().
# electron-builder.yml -- GitHub provider (default if GH_TOKEN set)
publish:
provider: github
owner: my-org
repo: my-app
# electron-builder.yml -- Generic HTTP server
publish:
provider: generic
url: https://releases.example.com/updates
# electron-builder.yml -- S3 bucket
publish:
provider: s3
bucket: my-app-releases
region: us-east-1
path: /releases
Key point: The first provider in the list is the auto-update source. Additional providers are publishing targets only. See examples/core.md for runtime setFeedURL() override.
Pattern 4: Update Channels (Stable/Beta/Alpha)
Channels distribute pre-release versions to specific user groups. Append -beta or -alpha to your package.json version to produce channel-specific metadata files.
{ "version": "2.1.0-beta" }
# electron-builder.yml
generateUpdatesFilesForAllChannels: true
// Switch channel at runtime
autoUpdater.channel = "beta";
// Setting channel automatically enables allowDowngrade
Key point: Users on alpha receive alpha, beta, and stable releases. Users on beta receive beta and stable. Users on latest (stable) only receive stable releases. See examples/channels-and-rollouts.md.
Pattern 5: Staged Rollouts
Roll out updates gradually by setting stagingPercentage in your metadata YAML file. The updater assigns each installation a persistent random ID and compares it against the percentage.
# latest.yml (manually edited after publishing)
version: 2.1.0
stagingPercentage: 10 # Ship to 10% of users first
Key point: Increment the version when pulling a broken staged release -- users already on the broken version will not downgrade to the same version number. See examples/channels-and-rollouts.md.
Pattern 6: Error Handling and Retry
Network failures during update checks are common. Wrap retry logic around the check and always handle the error event.
const MAX_RETRIES = 3;
const RETRY_DELAY_MS = 30_000; // 30 seconds
autoUpdater.on("error", (error) => {
log.error("Auto-update error:", error.message);
// Notify renderer for user-facing feedback
mainWindow.webContents.send("update-error", error.message);
});
Key point: The error event fires for network failures, signature verification failures, and corrupted downloads. Never ignore it -- unhandled errors in the updater crash the main process. See examples/core.md for retry with exponential backoff.
Pattern 7: Testing Locally
Use dev-app-update.yml and forceDevUpdateConfig to test the update flow without packaging.
# dev-app-update.yml (project root)
provider: generic
url: http://localhost:8080/updates
if (!app.isPackaged) {
autoUpdater.forceDevUpdateConfig = true;
}
Key point: You still need a local HTTP server serving the update artifacts (installer + latest.yml). Minio is commonly used as a local S3-compatible server for this purpose. See examples/testing.md.
Decision Framework
Which Update Approach?
Building with electron-builder?
+-- YES --> Use electron-updater (this skill)
+-- NO --> Building with Electron Forge?
+-- YES --> Using Squirrel maker?
| +-- YES --> Use Electron's built-in autoUpdater module
| +-- NO --> Can use electron-updater with custom config
+-- NO --> Distributing via app store?
+-- YES --> Use the store's native update mechanism
+-- NO --> Use electron-updater with generic provider
Which Provider?
Where are your releases hosted?
+-- GitHub Releases (public or private repo)
| +-- Use provider: github
+-- AWS S3 or compatible (MinIO, Backblaze B2)
| +-- Use provider: s3
+-- DigitalOcean Spaces
| +-- Use provider: spaces
+-- Any HTTP(S) server (Nginx, CDN, custom)
| +-- Use provider: generic
+-- Keygen (license-gated updates)
+-- Use provider: keygen
autoDownload: true vs false?
Should updates download automatically?
+-- App is small (<50 MB) and users expect seamless updates?
| +-- autoDownload: true (default) + checkForUpdatesAndNotify()
+-- App is large or users are on metered connections?
| +-- autoDownload: false + show download prompt in UI
+-- Enterprise environment with IT-managed rollouts?
+-- autoDownload: false + admin-controlled trigger
Detailed resources:
- examples/core.md - Setup, lifecycle events, manual download, providers, error handling with retry
- examples/channels-and-rollouts.md - Update channels, staged rollouts, channel switching
- examples/testing.md - Local testing, dev-app-update.yml, debugging with logging
- reference.md - API quick reference, event payloads, provider comparison, security checklist
RED FLAGS
Critical Issues:
- Calling
checkForUpdates()orcheckForUpdatesAndNotify()outsideapp.isPackagedguard -- causes errors and unnecessary network calls in development - Not handling the
errorevent onautoUpdater-- unhandled update errors crash the main process - Shipping unsigned macOS builds -- auto-update silently fails without code signing
- Calling
quitAndInstall()immediately without user confirmation -- forces restart, risks data loss - Using Electron's built-in
autoUpdatermodule instead of importing fromelectron-updater-- different API, different behavior, no Linux support
Architecture Issues:
- Running update logic in the renderer process --
electron-updatermust run in the main process only - Checking for updates on every app launch without a cooldown -- hammers the update server, especially with large user bases
- Not using
autoInstallOnAppQuitwhenautoDownloadis true -- users never get the update if they don't explicitly restart - Mixing Squirrel.Windows and NSIS updater patterns -- they are incompatible (Squirrel uses
.nupkgdelta files, NSIS uses blockmap-based differential downloads)
Staged Rollout Mistakes:
- Setting
stagingPercentage: 0expecting it to block all updates -- behavior is undefined at 0; use channels for access control instead - Not incrementing version when pulling a broken staged release -- users already on the broken version stay there
- Editing
stagingPercentageinlatest.ymlwithout re-signing -- signature validation fails
Common Mistakes:
- Forgetting
generateUpdatesFilesForAllChannels: truewhen using beta/alpha channels -- only the current channel's YAML is generated - Using
allowPrerelease: trueon the client instead of proper channels --allowPrereleaseonly works with GitHub provider and is less predictable than channels - Not setting
autoUpdater.loggerduring debugging -- update failures are silent without logging configured - Hardcoding update URLs instead of using
electron-builderpublish config -- the build process auto-generates correct metadata only when publish is configured
Gotchas & Edge Cases:
checkForUpdatesAndNotify()returnsnullwhenapp.isPackagedis false -- it silently skips in dev- Differential downloads (blockmap) only work for NSIS on Windows -- macOS and Linux always do full downloads
quitAndInstall(true)(silent mode) only works on Windows NSIS -- macOS ignores theisSilentparameter- The
download-progressevent does not fire when differential download is used -- only fires for full downloads - On Windows, the updater verifies the code signature of the downloaded installer by default (
verifyUpdateCodeSignature) -- unsigned updates are rejected setFeedURL()overrides the provider fromelectron-builderconfig at runtime -- useful for switching environments but can cause confusion if called unintentionally
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST guard update checks with app.isPackaged -- calling checkForUpdates() in development causes confusing errors and network calls to non-existent endpoints)
(You MUST handle the error event on the updater -- unhandled update errors crash the main process)
(You MUST code-sign macOS builds -- unsigned apps cannot auto-update and the updater silently fails)
(You MUST NOT call quitAndInstall() without confirming the user's intent -- forcing a restart mid-work causes data loss)
(You MUST use named constants for all intervals and timeouts -- no magic numbers in setInterval or retry logic)
Failure to follow these rules will cause silent update failures, crashes, or data loss for end users.