Keep your Electron app up to date automatically with update-electron-app, the free update.electronjs.org service and the built-in autoUpdater events.

Bishal Biswas
Self Learner
Why apps update themselves
You fixed a bug and released version 1.0.1. How do your users get it? If they have to visit your website, notice there is a new version, download it and reinstall, most of them never will. They keep the bug, and you keep getting the same bug reports.
An auto-updater fixes that. Your app quietly asks a server “is there anything newer than the version I am?”. If there is, it downloads the update in the background and installs it the next time the app restarts. Users always have the latest version without lifting a finger.
Think of a magazine subscription. You do not go to the shop every month to check whether a new issue is out. It simply arrives in your letterbox, and you read it when you are ready.
How Electron’s updater works
Electron has a built-in autoUpdater module in the main process. It is built on Squirrel, the same installer framework you met in Packaging and Making Installers with Forge:
- Windows: Squirrel.Windows. It updates apps that were installed with the Squirrel
Setup.exefromMakerSquirrel. - macOS: Squirrel.Mac. It downloads the
.zipof your app made byMakerZIP. The app must be signed, as in Code Signing and Notarization, or updates fail. - Linux: not supported. Linux users normally update apps through their package manager, Snap or Flatpak, which handle updates themselves.
The flow goes like this:
- Your app tells
autoUpdaterthe feed URL: the address of an update server. - It calls
checkForUpdates(). The server looks at the app’s current version and answers either “you are up to date” or “here is a newer version”. - If there is a newer version,
autoUpdaterdownloads it automatically in the background. - When the download finishes, it fires
update-downloaded. The update is installed the next time the app quits and restarts. You can also restart right away withquitAndInstall().
The update server: update.electronjs.org
You need a server that answers “is there a newer version?”. You could build your own, but if your app is open source on a public GitHub repository, there is a free one run by the Electron team: update.electronjs.org.
It reads your GitHub Releases. When you publish a new release with the right files attached, every installed copy of your app finds it. The requirements are:
- The repository is public on GitHub.
- Each release is a published GitHub release (not a draft), tagged with a version that follows semver, like
v1.0.1. - The release has the update files attached: for Windows the
.nupkgandRELEASESfiles fromMakerSquirrel, for macOS the.zipfromMakerZIP. - The macOS build is signed.
The next lesson, Publishing Releases to GitHub and App Stores, uploads exactly these files with one command.
The easy way: update-electron-app
The Electron team also publishes a small library, update-electron-app, that wires everything up for you. Install it as a normal dependency, because your packaged app needs it at run time:
npm install update-electron-app
Then add two lines to the Forge template’s src/main.js:
// src/main.js
import { app, BrowserWindow } from 'electron';
import path from 'node:path';
import started from 'electron-squirrel-startup';
import { updateElectronApp } from 'update-electron-app';
if (started) {
app.quit();
}
updateElectronApp();
// ...createWindow and the rest of the template stay the same
That is all. updateElectronApp():
- finds your GitHub repository from the
repositoryfield inpackage.json, - does nothing when the app is not packaged, so
npm startis unaffected, - does nothing on Linux,
- checks for updates when the app starts and then every 10 minutes,
- shows a dialog with Restart and Later buttons once an update has downloaded.
Make sure package.json has a repository field:
{
"name": "todo-buddy",
"productName": "Todo Buddy",
"version": "1.0.0",
"repository": "https://github.com/asha-rao/todo-buddy"
}
Options
You can be explicit about the repository and change the timing:
// src/main.js
import { updateElectronApp, UpdateSourceType } from 'update-electron-app';
updateElectronApp({
updateSource: {
type: UpdateSourceType.ElectronPublicUpdateService,
repo: 'asha-rao/todo-buddy',
},
updateInterval: '1 hour', // the minimum allowed is 5 minutes
logger: console, // print what the updater is doing
notifyUser: true, // show the Restart / Later dialog
});
With logger: console, the packaged app writes lines like checking-for-update and update-not-available to its log, which helps a lot when an update does not arrive.
If you host your update files yourself, on a plain static file host such as Amazon S3, use UpdateSourceType.StaticStorage with a baseUrl instead. On macOS that needs a RELEASES.json file, which MakerZIP creates when you set its macUpdateManifestBaseUrl option.
The flexible way: autoUpdater events
update-electron-app is perfect for many apps. Sometimes you want more control: a Check for Updates menu item, a progress message inside your window, or your own “update ready” banner instead of a dialog. Then use autoUpdater directly.
Step 1: set the feed URL
// src/main.js
import { app, autoUpdater } from 'electron';
function setUpUpdates() {
if (!app.isPackaged) return; // no updates during development
if (process.platform === 'linux') return; // not supported on Linux
const repo = 'asha-rao/todo-buddy';
const url = `https://update.electronjs.org/${repo}/${process.platform}-${process.arch}/${app.getVersion()}`;
autoUpdater.setFeedURL({ url });
}
The URL tells the server your repository, your platform and CPU (like win32-x64 or darwin-arm64) and your current version, so it can work out whether anything newer exists.
Step 2: listen to the events
autoUpdater tells you what it is doing through events:
// src/main.js
autoUpdater.on('checking-for-update', () => console.log('Checking for updates...'));
autoUpdater.on('update-available', () => console.log('Update found, downloading...'));
autoUpdater.on('update-not-available', () => console.log('You are up to date.'));
autoUpdater.on('error', (error) => console.error('Update error:', error.message));
autoUpdater.on('update-downloaded', (_event, releaseNotes, releaseName) => {
console.log('Downloaded', releaseName);
});
| Event | When it fires |
|---|---|
checking-for-update |
A check has started. |
update-available |
A newer version exists. The download starts automatically. |
update-not-available |
The app is already on the latest version. |
update-downloaded |
The new version is downloaded and ready to install. |
error |
Something failed: no internet, a bad feed URL, an unsigned Mac app. |
before-quit-for-update |
quitAndInstall() was called and the app is about to close. |
update-downloaded gives you the release notes and name. On Windows, only releaseName is filled in.
Step 3: check, then offer a restart
// src/main.js
import { dialog } from 'electron';
autoUpdater.on('update-downloaded', async (_event, _notes, releaseName) => {
const { response } = await dialog.showMessageBox({
type: 'info',
buttons: ['Restart now', 'Later'],
defaultId: 0,
message: `${releaseName || 'A new version'} is ready`,
detail: 'Restart the app to finish updating. Your work is saved.',
});
if (response === 0) autoUpdater.quitAndInstall();
});
app.whenReady().then(() => {
createWindow();
setUpUpdates();
setTimeout(() => autoUpdater.checkForUpdates(), 10_000); // first check after 10 seconds
setInterval(() => autoUpdater.checkForUpdates(), 60 * 60 * 1000); // then every hour
});
checkForUpdates()starts a check. Waiting a few seconds after startup keeps launch fast, as in Performance: Faster Startup and Less Memory.quitAndInstall()closes all windows, quits and installs. Only call it afterupdate-downloaded.- If the user picks Later, nothing is lost. A downloaded update is applied automatically the next time the app starts.
Warning:
quitAndInstall()closes windows without the usualbefore-quitevent firing first. If your app saves data on quit, also listen forautoUpdater’sbefore-quit-for-updateevent and save there too.
Sending progress to the window
To show update status in your page, forward the events through IPC, just like in IPC Part 3: Sending Messages to a Window:
// src/main.js
function sendStatus(text) {
for (const win of BrowserWindow.getAllWindows()) win.webContents.send('update:status', text);
}
autoUpdater.on('update-available', () => sendStatus('Downloading an update...'));
autoUpdater.on('update-downloaded', () => sendStatus('Update ready. It installs when you restart.'));
// src/preload.js
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('updates', {
onStatus: (callback) => ipcRenderer.on('update:status', (_event, text) => callback(text)),
});
Testing updates
You cannot test updates with npm start, because a development build is not installed and is not packaged. The real test looks like this:
- Set
"version": "1.0.0", then make and publish a release. - Install that release on a test machine with its installer.
- Change something visible, set
"version": "1.0.1", then make and publish again. - Start the installed 1.0.0 app and wait. You should get the “ready” dialog, and after restarting, see your change.
If nothing happens, add logger: console (or console.log calls in your event handlers) and read the log. Common causes are an unsigned Mac build, a draft release, missing .nupkg or RELEASES files, or a version number that did not go up.
An alternative: electron-builder and electron-updater
electron-builder is a different packaging tool from Electron Forge, popular in many projects. It comes with its own updater, electron-updater. It can update Linux AppImage builds, works with plain file servers and S3 out of the box, and reports download progress.
It is a separate toolchain with its own configuration and its own update files, so it does not mix with Forge’s makers and publishers. If you started with Forge, stay with autoUpdater and update-electron-app. Consider electron-builder only if you need something Forge’s approach cannot do, like auto-updating on Linux.
Common mistakes
Testing updates in development
update-electron-app skips everything when app.isPackaged is false, and Squirrel needs an installed app. Package, install and test the real thing.
An unsigned macOS app
Squirrel.Mac refuses to install updates for unsigned apps. You will see an error event. Sign and notarize your Mac builds.
Forgetting to bump the version
The server compares version numbers. If version 1.0.1 is still labelled 1.0.0 in package.json, nobody gets the update.
Calling checkForUpdates() too often
Each call can start a new download. Check at startup and then every hour or so, not every few seconds.
Quick recap
- Electron’s
autoUpdateruses Squirrel on Windows and macOS. Linux apps update through their package managers. - macOS updates require a signed app. Windows updates require a Squirrel install.
- update.electronjs.org is a free update server for public GitHub repositories, fed by your GitHub Releases.
updateElectronApp()fromupdate-electron-appsets everything up in one call, skips development builds, and shows a Restart dialog.- For full control, call
autoUpdater.setFeedURL({ url }), listen tochecking-for-update,update-available,update-not-available,update-downloadedanderror, then callquitAndInstall(). - Downloaded updates install on the next restart even if the user picks Later.
- Test with real installed builds and increasing version numbers.
- electron-builder’s
electron-updateris an alternative toolchain, including Linux AppImage updates.
// src/main.js: automatic updates with autoUpdater and update.electronjs.org
import { app, BrowserWindow, Menu, autoUpdater, dialog } from 'electron';
import path from 'node:path';
import started from 'electron-squirrel-startup';
if (started) app.quit(); // Squirrel install and uninstall runs on Windows
const REPO = 'asha-rao/todo-buddy'; // a public GitHub repository
const canUpdate = app.isPackaged && process.platform !== 'linux';
// --- Tell every window what the updater is doing
function sendStatus(text) {
for (const win of BrowserWindow.getAllWindows()) win.webContents.send('update:status', text);
}
// --- Feed URL and events
function setUpUpdates() {
if (!canUpdate) return;
const url = `https://update.electronjs.org/${REPO}/${process.platform}-${process.arch}/${app.getVersion()}`;
autoUpdater.setFeedURL({ url });
autoUpdater.on('checking-for-update', () => sendStatus('Checking for updates...'));
autoUpdater.on('update-available', () => sendStatus('Downloading an update...'));
autoUpdater.on('update-not-available', () => sendStatus('You have the latest version.'));
autoUpdater.on('error', (error) => sendStatus(`Update failed: ${error.message}`));
autoUpdater.on('update-downloaded', async (_event, _notes, releaseName) => {
sendStatus('Update ready. It installs when you restart.');
const { response } = await dialog.showMessageBox({
type: 'info',
buttons: ['Restart now', 'Later'],
defaultId: 0,
message: `${releaseName || 'A new version'} is ready`,
detail: 'Restart the app to finish updating.',
});
if (response === 0) autoUpdater.quitAndInstall();
});
setTimeout(() => autoUpdater.checkForUpdates(), 10_000); // after startup
setInterval(() => autoUpdater.checkForUpdates(), 60 * 60 * 1000); // every hour
}
// --- A "Check for Updates" menu item
function buildMenu() {
const template = [
...(process.platform === 'darwin' ? [{ role: 'appMenu' }] : [{ role: 'fileMenu' }]),
{ role: 'editMenu' },
{ role: 'viewMenu' },
{
label: 'Help',
submenu: [{ label: 'Check for Updates', enabled: canUpdate, click: () => autoUpdater.checkForUpdates() }],
},
];
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
}
// --- The template's window code
const createWindow = () => {
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: { preload: path.join(__dirname, 'preload.cjs') },
});
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
else mainWindow.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`));
};
app.whenReady().then(() => {
buildMenu();
createWindow();
setUpUpdates();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});
Was this explanation helpful?
Your feedback helps us improve the content for everyone.
Thank you for your feedback!
We appreciate you helping us improve.
What could we improve? (Optional)
Read aloud
Ready