If you're shipping an Electron desktop app to end users and you don't have auto-update wired in, you're going to keep getting "this is broken" bug reports from users who are actually running a version from three releases ago.
I've shipped 25 releases of GramShift Desktop (an Instagram automation indie SaaS) over the past several months, and bolting on auto-update from day one was easily one of the highest-ROI infrastructure decisions I've made. This post is the implementation core + the specific traps I walked into.
Setup: electron-updater + GitHub Releases
The stack is boring on purpose:
-
electron-updater(the npm package) handles update detection + download + install - GitHub Releases hosts the installer assets + a
latest.ymlmanifest - Clients periodically check
latest.ymlagainst their current version - On detection, they download and prompt the user to install
Release flow:
-
npm version patch(orminor/major) -
npm run buildproduces installers (.exe,.dmg) - Upload
.exe+latest.ymlto a new GitHub Release - Running clients see the new manifest on next check, prompt the user, install
The big upside: no separate update server. GitHub Releases acts as your CDN. The free tier handles indie-scale traffic without breaking a sweat.
Minimum-viable main-process code
The smallest thing that works in your Electron main process:
const { app, dialog } = require('electron');
const { autoUpdater } = require('electron-updater');
function setupAutoUpdate(mainWindow) {
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = true;
autoUpdater.on('update-available', (info) => {
dialog.showMessageBox(mainWindow, {
type: 'info',
title: 'Update available',
message: `Version ${info.version} is ready.`,
buttons: ['Update now', 'Later']
}).then((result) => {
if (result.response === 0) autoUpdater.downloadUpdate();
});
});
autoUpdater.on('update-downloaded', () => {
dialog.showMessageBox(mainWindow, {
title: 'Update ready',
message: 'Restart to apply the update.',
buttons: ['Restart', 'On next launch']
}).then((result) => {
if (result.response === 0) autoUpdater.quitAndInstall();
});
});
autoUpdater.checkForUpdates();
}
app.on('ready', () => {
const mainWindow = createWindow();
setupAutoUpdate(mainWindow);
});
autoDownload = false is deliberate. Users hate background traffic and disk usage they didn't consent to. Asking first ("Update available, want it now?") meaningfully improves the install rate vs. silently downloading and then asking to restart.
Trap 1: obfuscating your network module silently breaks production
This one cost me about three hours of confused debugging.
I was using JavaScript obfuscation to make reverse-engineering the desktop client harder. The obfuscator was configured to process most of src/, and I accidentally included api.js β the file that handles HTTP communication with the backend.
The symptom: reportActivity would silently fail in production. The dashboard would show "0 likes today" while the real database had the correct counts. Local dev was perfect because obfuscation only runs on the production build.
Meanwhile heartbeat and reportStats (which lived in the same api.js) were sending fine. The obfuscator's name-mangling had hit one specific function path differently from the others.
Fixes:
- Exclude network-layer files from obfuscation entirely. The marginal anti-reverse-engineering benefit is not worth the debugging cost when something goes wrong.
- Diff the production bundle against local for any file that handles auth, network, or persistence. A 30-second sanity check that would have saved my three hours.
- Watch your dashboards in real time for at least an hour after a release. This trap was visible in the data within minutes β I just wasn't looking.
Trap 2: forgetting to upload latest.yml
electron-updater needs latest.yml in the GitHub Release to detect new versions. It is shockingly easy to forget to upload it (or to upload only the installer).
If you forget, existing clients never detect the new release. They keep running the old version forever, and you don't notice until a user reports a bug "fixed in v1.4" while clearly running v1.3.
Mitigation: I now have a release script that, right before announcing internally, fetches the latest GitHub Release via the API and checks both .exe and latest.yml are present. If either is missing, it errors out before I post the announcement.
Trap 3: Windows SmartScreen + no code-signing cert
For Windows distribution without a code-signing certificate, your installer triggers "Windows protected your PC" SmartScreen warnings for the first few hundred downloads. A code-signing cert is about Β₯30-50k/year (USD ~$200-350), and that's the official fix.
For an indie launching with limited budget, I chose to skip the cert and instead:
- Publish a clear installation guide page with screenshots showing the "More info" β "Run anyway" path
- Link to that guide from the download button, not just the README
- Accept that there will be a small drop-off of users who bounce on the warning
Empirically, SmartScreen's reputation system also softens after enough downloads accumulate β so the early downloads where the warning is harshest are also when you have the least audience. The cost of acquiring the early users with the rougher experience is real but bounded.
Trap 4: full-version-number changelog mistakes are public forever
The flip side of "easy release flow" is that every release note ships permanently. Early on I wrote some release notes that referenced internal feature names that weren't worth disclosing, or had typos that aged poorly.
Habit I've adopted: every release note is "for users, not for me." Internal version comments go in CHANGELOG.md (private), user-visible notes go in the GitHub Release body, and the two are written separately. The discipline is mildly annoying but the permanence of public release notes is the kind of thing you don't notice until you wish you hadn't written something a year ago.
Why auto-update is a "support-cost-saving investment", not a "feature"
The thing that surprised me most: auto-update is not a feature your users will thank you for. They expect it. What it actually does is move you from the "what version are you on? please update" support pattern to "let me see the error" within a minute or two.
For a solo developer, that's the difference between a bug report consuming 30 minutes of triage vs. 5 minutes of actual diagnosis. The implementation work (about a day) pays itself back inside the first month.
If you're shipping Electron and don't have auto-update yet, the ROI is hard to beat. Wiring it in before your first paying customer means you never have to triage version drift.
What auto-update trap have you hit? Curious if other Electron devs have run into the obfuscation-name-mangling one β felt like an obscure failure mode at the time, but the post-mortem suggests it's probably more common than people talk about.
United States
NORTH AMERICA
Related News
Trump Calls Off AI Executive Order Over Concern It Could Weaken US Tech Edge
4h ago

Microservices Didn't Fail. People Did
4h ago

Meta Settles Lawsuit That Claimed Social Media Addiction Screwed Up Schools
4h ago

Centralized Authentication for a Multi-Brand Laravel Ecosystem
12h ago
Gizmo Guard - Safeguard Bot (Powered by Gemma4)
4h ago