<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Vinniharu</title>
    <description>The latest articles on DEV Community by Vinniharu (@vinniharu).</description>
    <link>https://dev.to/vinniharu</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3041044%2F79dc6b8c-620a-4028-b8ba-3d439f865ff1.jpeg</url>
      <title>DEV Community: Vinniharu</title>
      <link>https://dev.to/vinniharu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vinniharu"/>
    <language>en</language>
    <item>
      <title>How I Pushed a Critical Update to Every Company Branch Without Touching a Single Machine (Electron Auto-Update Deep Dive)</title>
      <dc:creator>Vinniharu</dc:creator>
      <pubDate>Thu, 02 Jul 2026 04:30:36 +0000</pubDate>
      <link>https://dev.to/vinniharu/how-i-pushed-a-critical-update-to-every-company-branch-without-touching-a-single-machine-electron-4pk1</link>
      <guid>https://dev.to/vinniharu/how-i-pushed-a-critical-update-to-every-company-branch-without-touching-a-single-machine-electron-4pk1</guid>
      <description>&lt;p&gt;A few months ago I built an internal desktop tool for my company using Electron. It runs on machines across &lt;strong&gt;every branch office&lt;/strong&gt; — different cities, different networks, different levels of IT literacy on the other end.&lt;/p&gt;

&lt;p&gt;Last week I needed to ship a fix. Not a "whenever people get around to it" fix — a &lt;strong&gt;mass update, same day, everywhere.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's the part that made that possible: I never had to visit a single branch, email a setup file, or ask anyone to click "install." I opened my terminal, ran two commands, and walked away. By the next morning, every machine running the app had already updated itself.&lt;/p&gt;

&lt;p&gt;That wasn't luck. It was because a few months earlier — before I needed it — I had wired up a proper auto-update system using &lt;code&gt;electron-updater&lt;/code&gt;. This post breaks down exactly how that system works, end to end, and how it turned what should have been a logistical nightmare into a non-event. Hopefully it saves someone else the trial and error.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with internal desktop software
&lt;/h2&gt;

&lt;p&gt;If you've ever shipped an internal Electron app to a non-technical audience spread across multiple physical locations, you already know the pain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You can't rely on people to download and run an installer.&lt;/li&gt;
&lt;li&gt;You can't assume IT support is on-site everywhere.&lt;/li&gt;
&lt;li&gt;You can't push through a public app store — it's internal software.&lt;/li&gt;
&lt;li&gt;"Just remote into each machine" does not scale past two or three branches.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The moment your user base is decentralized, manual distribution becomes your biggest bottleneck — not writing the fix itself.&lt;/p&gt;

&lt;p&gt;The fix for the bottleneck isn't a better installer. It's &lt;strong&gt;removing the installer from the loop entirely.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture: four layers, one goal
&lt;/h2&gt;

&lt;p&gt;The system I built is based on &lt;code&gt;electron-updater&lt;/code&gt; (from the &lt;code&gt;electron-builder&lt;/code&gt; ecosystem) pointed at a &lt;strong&gt;generic HTTP provider&lt;/strong&gt; — meaning updates are hosted on a plain web server I control via SFTP, with no dependency on GitHub Releases or a cloud storage vendor. That mattered a lot for internal company software that shouldn't be sitting in a public repo.&lt;/p&gt;

&lt;p&gt;There are four distinct layers doing the work:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Build &amp;amp; publish&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Compile the app, upload the installer and metadata to the server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Main process&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;On launch, silently check for a newer version and download it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Preload bridge&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Safely relay update events from the sandboxed main process to the UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Renderer UI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Show the user what's happening, without letting them stop it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Let's walk through each one — and where the "mass update to every branch" moment actually happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Building and publishing a release
&lt;/h2&gt;

&lt;p&gt;Everything starts with a single script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run publish
&lt;span class="c"&gt;# → npm run build &amp;amp;&amp;amp; npm run upload&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Compiling the app
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;nextron build&lt;/code&gt; compiles both the Next.js renderer and the Electron main process into a &lt;code&gt;dist/&lt;/code&gt; folder. &lt;code&gt;electron-builder&lt;/code&gt; handles packaging based on &lt;code&gt;electron-builder.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;publish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;generic&lt;/span&gt;       &lt;span class="c1"&gt;# plain HTTP server, no GitHub / S3&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://&amp;lt;server&amp;gt;/updates/&lt;/span&gt;
    &lt;span class="na"&gt;channel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;latest&lt;/span&gt;
    &lt;span class="na"&gt;useMultipleRangeRequest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This one config block is the reason I own the entire update pipeline. There's no third-party service that could rate-limit me, gate access behind auth I don't control, or go down at the wrong moment. It's just a server I already trust.&lt;/p&gt;

&lt;p&gt;Every build also auto-generates a &lt;strong&gt;&lt;code&gt;latest.yml&lt;/code&gt;&lt;/strong&gt; file — the single source of truth for the update system. It contains the new version number, the installer filename, a SHA-512 hash for integrity verification, and the release date. This file is what every branch's app will silently check against.&lt;/p&gt;

&lt;h3&gt;
  
  
  Uploading with resume logic
&lt;/h3&gt;

&lt;p&gt;The upload script (&lt;code&gt;scripts/upload-update.mjs&lt;/code&gt;) pushes everything to the server over SFTP, and it's smarter than a blind file copy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;For each artifact, it sends a &lt;code&gt;HEAD&lt;/code&gt; request to check what's already on the server.&lt;/li&gt;
&lt;li&gt;If the remote file matches the local size exactly, it's skipped — already complete.&lt;/li&gt;
&lt;li&gt;If it's a partial upload, it resumes instead of restarting.&lt;/li&gt;
&lt;li&gt;YAML metadata files are &lt;strong&gt;always overwritten&lt;/strong&gt;, even if the size looks identical — because stale metadata is the one thing that would silently break every client at once.&lt;/li&gt;
&lt;li&gt;Missing files get a full upload.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Given that some of our branches sit on genuinely flaky connections, the SSH session is also tuned for resilience — a keepalive probe every 15 seconds, tolerating up to 10 missed probes (roughly 150 seconds) before giving up. That's the kind of detail you don't think about until a publish fails halfway through at 6 PM and you're debugging a timeout instead of going home.&lt;/p&gt;

&lt;p&gt;This is the entire "developer side" of a mass update. I run &lt;code&gt;npm run publish&lt;/code&gt;. That's it. Everything past this point happens on machines I will never log into.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: The main process — where the magic actually happens
&lt;/h2&gt;

&lt;p&gt;This is the layer that turns a file sitting on a server into an update running silently on a machine in a branch office 400 kilometers away.&lt;/p&gt;

&lt;p&gt;On every app launch, in production builds only, the following runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;autoUpdater&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;log&lt;/span&gt;
&lt;span class="nx"&gt;autoUpdater&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;level&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;info&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="nx"&gt;autoUpdater&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;autoDownload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;          &lt;span class="c1"&gt;// no prompt — just download&lt;/span&gt;
&lt;span class="nx"&gt;autoUpdater&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;autoInstallOnAppQuit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="nx"&gt;autoUpdater&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkForUpdates&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Initial update check failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;autoDownload: true&lt;/code&gt; is the key decision here. There's no "a new version is available, click to download" dialog for the user to ignore. &lt;code&gt;electron-updater&lt;/code&gt; fetches &lt;code&gt;latest.yml&lt;/code&gt;, compares versions, and if there's a newer one, it starts downloading immediately.&lt;/p&gt;

&lt;p&gt;Six events are wired up, each forwarded to the renderer over IPC so the UI can react:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;checking-for-update&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Started polling the server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;update-available&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A newer version exists — download begins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;update-not-available&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Already current&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;download-progress&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Live bytes/percent/speed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;update-downloaded&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Installer verified and ready&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;error&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Something went wrong — logged, UI recovers gracefully&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Once the download finishes and the hash is verified:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;autoUpdater&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;update-downloaded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;mainWindow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webContents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;update-downloaded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;autoUpdater&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;quitAndInstall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three seconds after the "installing" message renders, &lt;code&gt;quitAndInstall(true, true)&lt;/code&gt; fires — silently, no confirmation dialog, force-closing all windows and running the installer immediately. The app comes back up seconds later on the new version.&lt;/p&gt;

&lt;p&gt;No one at any branch had to know an update even shipped that day. It just... happened, in the background, between one launch and the next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: The preload bridge
&lt;/h2&gt;

&lt;p&gt;Electron's sandboxed renderer can't touch Node or Electron APIs directly, and it shouldn't — that's a security boundary you don't want to punch a hole through carelessly. The preload script exposes a narrow, controlled interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;contextBridge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exposeInMainWorld&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ipc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subscription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;ipcRenderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ipcRenderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ...send, invoke&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The returned cleanup function matters more than it looks — without it, every hot reload or component remount during development leaks a listener, and eventually you're debugging duplicate update dialogs firing three times for no reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: The renderer — giving users just enough visibility
&lt;/h2&gt;

&lt;p&gt;Silent updates shouldn't mean &lt;em&gt;invisible&lt;/em&gt; updates. If someone's mid-task and the app is about to restart itself, they deserve to see that coming.&lt;/p&gt;

&lt;p&gt;Global listeners are registered once in &lt;code&gt;_app.tsx&lt;/code&gt;, driving three pieces of state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;update-available  →  modal appears, "downloading" stage, progress starts at 0
download-progress →  progress bar updates live
update-error      →  modal quietly disappears, no user-facing failure noise
update-downloaded →  modal switches to "installing" stage
                      (3s later, the app restarts itself)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The modal itself is a fixed full-screen overlay with a backdrop blur — genuinely hard to miss, but not alarming. The downloading stage shows a live progress bar, transferred/total bytes, download speed, and an ETA calculated from remaining bytes divided by bytes-per-second. The installing stage swaps to a green indicator, a full progress bar, and a plain warning not to close the window because it's about to close itself anyway.&lt;/p&gt;

&lt;p&gt;That's really the whole design philosophy: &lt;strong&gt;give people visibility, take away the decision.&lt;/strong&gt; For internal software running business-critical workflows across branches, "click here to update" is a support ticket generator. Silent-but-visible is the sweet spot.&lt;/p&gt;

&lt;h2&gt;
  
  
  The day it actually mattered
&lt;/h2&gt;

&lt;p&gt;Here's the payoff. A bug surfaced that needed fixing across the board — not "roll it out gradually," but everywhere, same day. In the old world, that would have meant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Packaging a new installer&lt;/li&gt;
&lt;li&gt;Emailing it to whoever manages each branch's machines&lt;/li&gt;
&lt;li&gt;Hoping someone actually ran it before end of day&lt;/li&gt;
&lt;li&gt;Following up for a week with the branches that didn't&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead, this is what actually happened:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Developer:
  npm run build     → new version compiled, latest.yml regenerated with new hash
  npm run upload    → SFTP pushes installer + metadata to the server

Every branch, independently, on next app launch:
  checkForUpdates() → sees new version in latest.yml
  update-available  → download starts automatically, no click needed
  download-progress → user sees a clean progress bar
  update-downloaded → app quits and reinstalls itself
  App relaunches    → running the fixed version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I didn't touch a single machine. I didn't send a single file over email or Slack. Every branch pulled the fix on its own schedule, the moment someone opened the app, with zero coordination required from me or from them.&lt;/p&gt;

&lt;p&gt;That's the actual return on investment of building the update pipeline &lt;em&gt;before&lt;/em&gt; you need it under pressure. The infrastructure work happens once, calmly, months in advance. The payoff shows up exactly when you least have time to improvise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways for your own Electron app
&lt;/h2&gt;

&lt;p&gt;If you're building internal or B2B desktop software with &lt;code&gt;electron-updater&lt;/code&gt;, a few lessons from this stood out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use the generic HTTP provider if you need full control.&lt;/strong&gt; Not every internal tool belongs on GitHub Releases or a vendor's CDN — owning the server means owning the rollout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always overwrite metadata files on upload.&lt;/strong&gt; A stale &lt;code&gt;latest.yml&lt;/code&gt; with a fresh installer is a worse failure mode than no update at all — clients will silently think they're current when they're not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build resume logic into your upload step.&lt;/strong&gt; Flaky connections are a fact of life for distributed offices; don't let a dropped SFTP session mean a corrupted release.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silent auto-download, visible progress.&lt;/strong&gt; Don't ask non-technical users to make an update decision — but don't hide the fact that the app is about to restart itself either.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify with a hash, always.&lt;/strong&gt; The SHA-512 check in &lt;code&gt;latest.yml&lt;/code&gt; is what makes &lt;code&gt;autoDownload: true&lt;/code&gt; safe enough to trust across machines you'll never personally inspect.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Closing thought
&lt;/h2&gt;

&lt;p&gt;The best infrastructure work is invisible right up until the one day it saves you. Building the auto-update pipeline felt like a "nice to have" the day I shipped it. Months later, on the day I needed to patch every branch in the company at once, it was the only reason that day didn't turn into a week.&lt;/p&gt;

&lt;p&gt;If your desktop app doesn't have this yet — and it's going to live on more than one machine you don't control — this is the piece to build before you need it, not after.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Stack used: &lt;code&gt;electron-updater&lt;/code&gt; (&lt;code&gt;^6.8.3&lt;/code&gt;), &lt;code&gt;electron-builder&lt;/code&gt; (&lt;code&gt;^26.8.1&lt;/code&gt;), &lt;code&gt;electron-log&lt;/code&gt; (&lt;code&gt;^5.4.3&lt;/code&gt;), Next.js via &lt;code&gt;nextron&lt;/code&gt;, deployed to a generic HTTP server over SFTP.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Have you dealt with rolling out updates across machines you don't control? Curious whether others went the generic-provider route or leaned on something like S3/GitHub Releases instead — and how you handled staged rollouts if the update ever needed to be pulled back. Drop your approach below 👇&lt;/p&gt;

</description>
      <category>electron</category>
      <category>devops</category>
      <category>javascript</category>
      <category>node</category>
    </item>
  </channel>
</rss>
