How to Setup Auto Update for Electron App
Shipping an Electron app is easy. Keeping every user on the latest version is the hard part. Unlike apps on the App Store or Google Play, a desktop app you distribute yourself has no built-in update channel — so you have to build one.
This guide shows how to add automatic updates to an Electron app using faynoSync, a self-hosted, open-source update server. We'll cover two approaches:
- A custom update flow — you call faynoSync's
checkVersionAPI yourself and control the UX end to end. - Native
electron-updaterintegration — faynoSync speaks the electron-builder protocol, so the standard updater works out of the box.
We'll also cover the parts most tutorials skip: code signing, critical updates, changelogs, and what to do when updates silently don't work.
Two ways to auto-update an Electron app
| Custom flow | electron-updater (electron-builder) | |
|---|---|---|
| Control over UX | Full | Limited to autoUpdater events |
| Install handling | You implement it | Automatic (download + relaunch) |
| Code signing required | No (for download links) | Yes (macOS/Windows) |
| Best for | Custom installers, portable apps | Standard packaged apps (.dmg/.nsis/.AppImage) |
| faynoSync support | manual updater | electron-builder updater |
If you just want the app to update itself with minimal code, go with electron-updater. If you need full control over the download and install step, use the custom flow. Both talk to the same faynoSync instance.
Prerequisites 📋
Before we begin, make sure you have:
- A running faynoSync instance — see Getting Started
- Published versions 0.0.1 and 0.0.2 of your app
- Created an app in faynoSync named "myapp"
- Created a channel in faynoSync named "nightly"
- Created a platform in faynoSync named (darwin/linux/windows)
- Created an architecture in faynoSync named (amd64/arm64)
Approach A: Custom update flow
This approach gives you complete control: you ask faynoSync whether an update exists and decide how to present it. Under the hood it uses the default manual updater, which returns plain JSON with direct download URLs.
1. Initialize Your Project
First, let's create a new project with the following package.json:
{
"name": "myapp",
"version": "0.0.1",
"description": "Hello world app for testing faynoSync",
"main": "main.js",
"scripts": {
"start": "electron ."
},
"keywords": [],
"author": "Example Author",
"license": "ISC",
"dependencies": {
"node-fetch": "2.6.9"
},
"devDependencies": {
"electron": ">=23.3.13"
},
"engines": {
"npm": ">=8.19.3",
"node": ">=18.13.0"
}
}
2. Create Basic Files
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title id="title"></title>
</head>
<body>
<h1 id="label">Hello, world!</h1>
</body>
</html>
config.js
const packageJson = require('./package.json');
module.exports = {
app_name: packageJson.name,
version: packageJson.version,
channel: "nightly",
owner: "example",
};
The Magic of Auto-Updates ✨
Let's create our main.js file step by step:
1. Import Required Modules
const { app, BrowserWindow, dialog, shell } = require('electron');
const fetch = require('node-fetch');
const os = require('os');
const { version, app_name, channel, owner } = require('./config.js');
const fs = require('fs');
2. Platform Detection
function getLinuxDistributionFamily() {
let distroFamily = 'Linux';
try {
const releaseInfo = fs.readFileSync('/etc/os-release', 'utf8');
const match = releaseInfo.match(/^ID(?:_LIKE)?=(.*)$/m);
if (match) {
const idLike = match[1].trim().toLowerCase();
if (idLike.includes('rhel') || idLike.includes('fedora') || idLike.includes('centos')) {
distroFamily = 'RHEL';
} else if (idLike.includes('debian') || idLike.includes('ubuntu') || idLike.includes('kali')) {
distroFamily = 'Debian';
}
}
} catch (err) {
console.error('Error getting Linux distribution family:', err);
}
return distroFamily;
}
3. Update Choice Window
function createChoiceWindow(updateOptions) {
const win = new BrowserWindow({
width: 600,
height: 400,
webPreferences: {
nodeIntegration: true,
},
});
win.loadURL(`data:text/html,
<html>
<body>
<h2>Choose an update package:</h2>
<ul>
${updateOptions
.map(
(option, index) =>
`<li><a id="option-${index}" href="${option.url}">${option.name}</a></li>`
)
.join('')}
</ul>
<script>
const { shell } = require('electron');
document.addEventListener('click', (event) => {
if (event.target.tagName === 'A') {
event.preventDefault();
shell.openExternal(event.target.href);
}
});
</script>
</body>
</html>`
);
return win;
}
4. Update Check Function
function checkUpdates() {
let url = `http://localhost:9000/checkVersion?app_name=${app_name}&version=${version}&platform=${os.platform()}&arch=${os.arch()}&owner=${owner}`;
if (channel !== undefined) {
url += `&channel=${channel}`;
}
fetch(url, { method: 'GET' })
.then((res) => res.json())
.then((data) => {
console.log(data);
if (data.update_available) {
const message = data.critical
? `A critical update is available. Please update now.`
: `You have an older version. Would you like to update your app?`;
dialog.showMessageBox({
type: 'question',
title: data.critical ? 'Critical update' : 'Update available',
message: message,
detail: data.changelog || undefined,
buttons: ['Yes', 'No'],
defaultId: 0,
}).then(({ response }) => {
if (response === 0) {
const updateOptions = [];
for (const key in data) {
if (key.startsWith('update_url_')) {
updateOptions.push({ name: key.substring(11).toUpperCase(), url: data[key] });
}
}
const choiceWindow = createChoiceWindow(updateOptions);
}
});
}
})
.catch(() => {});
}
Notice we now read two extra fields from the response: critical and changelog. faynoSync returns these automatically — critical lets you force the prompt, and changelog shows users what changed. We'll cover them in detail below.
5. Main Window Creation
function createWindow() {
let osName = os.platform();
let pcArch = os.arch();
if (osName === 'linux') {
osName = getLinuxDistributionFamily();
}
const title = `${app_name} - v${version} (${osName}-${pcArch})`;
let win = new BrowserWindow({
width: 400,
height: 300,
webPreferences: {
nodeIntegration: true,
},
});
win.setTitle(title);
win.loadFile('index.html');
win.on('closed', () => {
win = null;
});
checkUpdates();
}
app.whenReady().then(createWindow);
Running Your App 🏃♂️
To start your app, simply run:
npm start
If a newer version exists, you'll see something like this in your logs:
{
"critical": false,
"update_available": true,
"update_url_dmg": "http://localhost:9010/cb-faynosync-s3-public/myapp-example/nightly/darwin/arm64/myapp-0.0.2.0.dmg"
}
And in your app's UI, you'll see a notification about the available update. After agreeing, the user downloads and installs the new version.
Approach B: Native electron-updater integration
Most production Electron apps are packaged with electron-builder and use its electron-updater module. The good news: faynoSync ships an electron-builder updater that returns the exact YAML feed (latest.yml, latest-mac.yml, latest-linux.yml) the updater expects — so you don't need GitHub Releases or an S3 generic provider.
1. Upload builds for the electron-builder updater
When uploading, pass the updater parameter so faynoSync isolates the artifacts and generates the matching *.yml:
curl -X POST --location 'http://localhost:9000/upload' \
--header 'Authorization: Bearer <jwt_token>' \
--form 'file=@"/path/to/myapp-0.0.2.dmg"' \
--form 'file=@"/path/to/latest-mac.yml"' \
--form 'data="{\"app_name\":\"myapp\",\"version\":\"0.0.2\",\"channel\":\"nightly\",\"publish\":true,\"platform\":\"darwin\",\"arch\":\"arm64\",\"updater\":\"electron-builder\"}"'
2. Point electron-updater at faynoSync
In your app, configure the feed to use the generic provider and faynoSync's electron-builder endpoint:
const { autoUpdater } = require('electron-updater');
autoUpdater.setFeedURL({
provider: 'generic',
url: 'http://localhost:9000/checkVersion?app_name=myapp&channel=nightly&platform=darwin&arch=arm64&owner=example&updater=electron-builder',
});
faynoSync responds with electron-builder-compatible YAML:
version: 0.0.2
files:
- url: myapp-0.0.2.dmg
sha512: <sha512_hash>
size: <file_size>
path: myapp-0.0.2.dmg
sha512: <sha512_hash>
releaseDate: '2026-01-15T10:00:00.000Z'
See the Updaters Support doc for the full list of supported updaters (squirrel_windows, squirrel_darwin, tauri, and more).
3. Wire up the autoUpdater events
const { app, dialog } = require('electron');
const { autoUpdater } = require('electron-updater');
app.whenReady().then(() => {
autoUpdater.checkForUpdates();
});
autoUpdater.on('update-available', (info) => {
console.log('Update available:', info.version);
});
autoUpdater.on('download-progress', (progress) => {
console.log(`Downloaded ${Math.round(progress.percent)}%`);
});
autoUpdater.on('update-downloaded', () => {
dialog.showMessageBox({
type: 'info',
title: 'Update ready',
message: 'A new version has been downloaded. Restart to apply it?',
buttons: ['Restart', 'Later'],
}).then(({ response }) => {
if (response === 0) autoUpdater.quitAndInstall();
});
});
autoUpdater.on('error', (err) => {
console.error('Update error:', err);
});
That's the whole flow: electron-updater downloads the package in the background and quitAndInstall() swaps the binary and relaunches.
Code signing and notarization
electron-updater will refuse to install unsigned updates on macOS and Windows. This is the single most common reason auto-updates "work in dev but not in production." Before shipping:
- macOS — sign with a Developer ID certificate and notarize the app, otherwise Gatekeeper blocks it and the updater silently fails. With electron-builder, set
mac.notarizeand provideAPPLE_ID/APPLE_APP_SPECIFIC_PASSWORD/APPLE_TEAM_IDin your build environment. - Windows — sign the installer with an Authenticode certificate (EV or OV). Unsigned installers trigger SmartScreen warnings and break differential updates. If you don't want to buy a traditional certificate, Microsoft's Azure Trusted Signing offers a fully managed signing service for about $9.99/month — a cheap, modern alternative for individual developers and small teams.
- Linux — AppImage updates don't require signing, which makes Linux the easiest target to test the flow first.
The custom flow (Approach A) sidesteps the updater's signature checks because you open download links yourself — but you still owe your users signed binaries.
Critical updates, changelogs, and required intermediate builds
faynoSync's checkVersion response carries three fields that make updates smarter:
critical— mark a release as critical at upload time, and the API flags it here. Use it to make the update prompt non-dismissible.changelog— markdown release notes returned alongside the update, so you can show "What's new" without a second request.is_intermediate_required— an informational flag. When a mandatory intermediate build exists between the user's version and latest, faynoSync returns the download link for that intermediate version (not latest) and sets this field totrue. The intermediate build must be installed before the latest one — it's used for migrations that can't be skipped, such as database schema changes.
{
"update_available": true,
"critical": true,
"is_intermediate_required": true,
"changelog": "### Changelog\n\n- Added feature X\n- Fixed bug Y",
"update_url_dmg": "https://<bucket>.s3.amazonaws.com/myapp/stable/darwin/amd64/myapp-0.0.5.dmg"
}
If is_intermediate_required is true, prompt the user to install the returned version, then check again — repeat until they reach latest. Read more in Required Intermediate Build.
Troubleshooting
| Symptom | Likely cause |
|---|---|
update_available is always false | version in the request matches the latest, or wrong channel/platform/arch |
| electron-updater says "No published versions" | The *.yml feed wasn't uploaded, or updater=electron-builder was omitted on upload |
| Update downloads but won't install (macOS/Windows) | App or installer isn't code signed / notarized |
| Works locally, fails in packaged app | setFeedURL points to localhost — use your public faynoSync URL in production |
| 401 on upload | Missing or expired JWT — re-authenticate against /login |
Tip: run faynoSync's /info/latest endpoint manually with curl to confirm the server returns what you expect before debugging the client.
How to try faynoSync?
-
Follow the Getting Started guide: 👉 https://faynosync.com/docs/getting-started
-
Create your app using the REST API or web dashboard: 📦 API Docs: https://faynosync.com/docs/api 🖥️ Dashboard UI: https://github.com/ku9nov/faynoSync-dashboard
-
Upload at least two versions of your application.
-
Check for updates with this simple request: 📡
/info/latest
Related reading
- Fetch Latest Version of App — Smart Update Links
- Performance Mode — Speed Up Your API
- Updaters Support
Need Help? 🤝
If you have any questions or need assistance:
- Check out our documentation
- Create an issue on GitHub
If you find this project helpful, please consider subscribing, leaving a comment, or giving it a star, create an Issue or feature request on GitHub. Your support keeps the project alive and growing 💚
