Cantürk Çelikok
All postsDesktop Development

How to Set Up Automatic Updates in a Desktop App

Cantürk Çelikok • • 6 min read

How to Set Up Automatic Updates in a Desktop App

Why automatic updates are a must

In a web app, everyone starts using the new version the moment you deploy it. On the desktop it's the other way round: the app is installed once and updated whenever the user feels like it. In practice, that means never. Few users go back to the download page to install a new version; most keep using the version they first downloaded until a bug stops them.

For an app like Nerdesin, which talks to iOS devices, this is even more critical. When something small changes on Apple's side you can ship a fix straight away, but users on the old version keep hitting the problem and report a bug you've "already fixed". So for me automatic updates aren't a nice-to-have. They're what keeps the product working.

In this post I walk through the update flow I built for an Electron-based Windows app, in the order I did it. The examples use electron-builder and electron-updater, but the logic is the same with other tools.

The parts of the flow

Automatic updates have four parts, and if any of them is missing the system either doesn't work at all or works insecurely:

  1. Packaging: each release produces an installer and a manifest that describes it (latest.yml).
  2. Release server: an HTTPS address where these files live. It can be GitHub Releases, S3-compatible storage or a folder on your own server.
  3. Client side: when the app starts it reads the manifest, and if there's a new version it downloads and verifies it.
  4. Install time: when and how the downloaded version is installed. This is the part that affects the user experience most.

1. Packaging and the release address

On every build, electron-builder writes a latest.yml next to the installer. It contains the version number, file name, size and a SHA-512 hash. The client compares the hash of the file it downloaded against it, so a file that was corrupted or tampered with on the way is never installed.

Because I host on my own server, I use the generic provider:

# electron-builder.yml
appId: app.nerdesin.desktop
win:
  target: nsis
publish:
  provider: generic
  url: https://updates.example-domain.com/nerdesin/

Once the build finishes, you just upload the .exe, .exe.blockmap and latest.yml from the dist folder to that address. The order matters here: upload the installer first and latest.yml last. If you do it the other way round, the manifest points to the new version before the file is in place, and anyone who opens the app in the meantime gets an error.

The .blockmap file is there for a reason too. It lets the client download only the blocks that changed instead of the whole new version (a differential update), which makes a real difference for users on slow connections.

2. Code signing: the step you can't skip

When someone downloads an unsigned Windows installer, SmartScreen shows the "Windows protected your PC" warning. To a non-technical user, that screen means the app is a virus, and the installation will most likely stop right there.

Getting a code signing certificate costs money and requires company or identity verification, but for a commercial desktop product it's unavoidable. Two things to watch:

  • Sign every release with the same certificate. On Windows, SmartScreen reputation builds up over time and is tied to the certificate and the file. Change the certificate and that reputation can reset.
  • Sign the update file as well. On Windows, electron-updater can compare the downloaded file's signature with the installed app's. That's a second line of defence: even if your release server is compromised, a file signed by someone else won't be installed.

3. Client side: check quietly, notify clearly

The code on the app side is actually short. The real decision is when you interrupt the user:

import { app, dialog } from "electron";
import { autoUpdater } from "electron-updater";
import log from "electron-log";
 
autoUpdater.logger = log;
autoUpdater.autoDownload = true;
autoUpdater.autoInstallOnAppQuit = true;
 
autoUpdater.on("update-downloaded", async (info) => {
  const { response } = await dialog.showMessageBox({
    type: "info",
    buttons: ["Restart now", "Later"],
    defaultId: 0,
    cancelId: 1,
    message: `A new version is ready: ${info.version}`,
    detail: "If you don't restart now, the update will be installed when you close the app.",
  });
  if (response === 0) autoUpdater.quitAndInstall();
});
 
app.whenReady().then(() => {
  if (app.isPackaged) autoUpdater.checkForUpdates().catch((err) => log.error(err));
});

With this setup the download happens quietly in the background. The user sees a single question only once the file is fully downloaded and verified, and even if they say "Later", the update is installed the next time they close the app. I never interrupt anyone's work; with an app like Nerdesin, restarting on its own while a route is playing would be unacceptable.

The app.isPackaged check matters too: update checks don't run in development, so you never accidentally download the live version over your own dev build. To test the flow during development you can use a dev-app-update.yml file and the forceDevUpdateConfig option.

Always log errors. The file electron-log keeps is the only real evidence you can ask for from a user who says "the update never arrives". Most of the time the cause is a corporate proxy or an antivirus blocking the download, and you can only see that in the log.

4. Staged rollouts and a recovery plan

When you get a desktop update wrong, there's no "undo" button like on the web. The broken version is already installed on the user's machine, and the only way to fix it is to ship another update. So I take two precautions:

Staged rollouts. When you add a stagingPercentage field to latest.yml, the update is only offered to that percentage of users. I open a new version to a small group first, watch the bug reports and, if nothing comes up, raise the percentage.

version: 2.4.0
files:
  - url: Nerdesin-Setup-2.4.0.exe
    sha512: ...
    size: ...
path: Nerdesin-Setup-2.4.0.exe
sha512: ...
releaseDate: '2026-09-08T10:00:00.000Z'
stagingPercentage: 20

Roll forward. If a release has a serious problem, I repackage the old version with a higher version number and ship that. The client only updates to a "newer" version, so you can't go back from 2.4.0 to 2.3.9, but you can ship 2.3.9's code as 2.4.1. To do this quickly you need every release tagged in Git and a build you can reproduce with a single command.

Mandatory updates

Sometimes letting an old version keep running is genuinely harmful: an external service's API has changed, or the old version has a security hole. For these cases I keep a small endpoint on the server, and the app asks it for the lowest supported version at startup. If its own version is below that, instead of opening normally it shows an "update required" screen and starts the download.

I use this very rarely. If you make every update mandatory, users have to deal with something every time they open the app, and the whole point of automatic updates is lost.

Checklist

The list I use when setting up the update flow in a new desktop project:

  • The release address is HTTPS and hard-coded into the app.
  • The installer and the updates are signed with the same certificate.
  • The manifest is uploaded last, and the upload script guarantees it.
  • Downloads happen in the background; installs happen with the user's consent or on quit.
  • Update errors are written to a file, and the user can find that file easily.
  • New versions are rolled out in stages.
  • The roll-forward steps for a bad release are written down and tested.

Once this list is done before the first release, every release after that is a few minutes' work. I describe how I work on desktop app projects in more detail on the desktop app development page.

Cantürk Çelikok

Freelance full stack developer based in Ankara, Türkiye. He built live products such as PlusTurk, Nerdesin, Ayda POS and Songnaria Coffee end to end, and shares the notes from those projects here.

About me