LabHub

Blog

Building Updatable Solutions: From Installer Wizards to Safe Auto-Updates

한국어English日本語中文

Introduction — Installing Is Only the Beginning

The real problems start the moment you ship software to a user. The first install happens once, but updates continue for the entire life of the product. To fix bugs, add features, and patch security holes, you have to safely swap thousands or tens of thousands of already-deployed installs over to a new version.

This post covers two things together. One is the UX of the installer wizard the user meets first; the other is the auto-update mechanism running quietly behind it. Whether it is a desktop app, a mobile app, or a web PWA, the principles of building an "updatable solution" are surprisingly similar. Download, verify, swap atomically, and roll back on failure. Those four steps are the whole story.

Part 1. Wizard UX

Multi-Step Flows and Per-Step Validation

A wizard is a UI that splits a complex setup into several steps. Instead of dumping every option onto one screen, it guides the user one step at a time. Good wizards share a common skeleton.

Preview, Dry-Run, and Rollback on Failure

Installing or updating changes the system. Showing what will change before the user hits "Apply" builds trust.

Resumability

The network can drop in the middle of a large download, or the user might close the window by accident. Making them start over every time is exhausting. Resumability is the ability to save the state at the point of interruption and pick up from there when reopened. Downloads resume with HTTP Range requests, and the wizard's inputs are stashed locally.

Part 2. Update Mechanisms

Semantic Versioning and Update Channels

To manage updates, you first need to version things systematically. Semantic versioning (SemVer) splits a version into three parts: MAJOR.MINOR.PATCH. Bump MAJOR when compatibility breaks, MINOR when features are added, PATCH when only bugs are fixed. From this rule, a client can tell whether an update is a small safe fix or a large change to be careful with.

On top of that, add update channels. Even for the same product, keep several streams such as stable, beta, and nightly, so a new version reaches only the users willing to take on risk first. Most users stay on stable; early adopters move to beta.

Full vs Delta/Differential Updates

The simplest approach is a full update: download the entire new version and swap it in. Easy to implement, but if the app is hundreds of MB, even a one-line fix means downloading hundreds of MB every time.

Delta/differential updates remove that waste. You download only the diff between the old and new versions and merge it locally. There are well-known tools for this.

Delta updates save a lot of bandwidth, but the patch only applies if the previous version is exactly the expected one. So teams usually prepare multiple patches from several versions to the latest, or make delta automatically fall back to a full update when it fails.

Atomic Swap and Rollback

The riskiest moment in an update is when files are actually swapped. What if the power fails between deleting the old file and writing the new one? Atomic swap prevents this.

The key is to "prepare everything fully, then flip exactly once at the end." Install the new version alongside the old, whole (say, in a v2 folder), and once verification passes, switch the pointer that indicates "the current version" (a symlink or a setting) from v1 to v2 in a single move. Using the file system's atomic rename, at every instant the user sees either an intact v1 or an intact v2. A half-mixed state never exists. If something goes wrong, just point the pointer back to v1 and the rollback is done.

  /app/current  ->  /app/versions/1.4.0   (switch via atomic rename)
                    /app/versions/1.5.0   <- pre-installed + verified

  after switch:
  /app/current  ->  /app/versions/1.5.0
  on failure, point current back to 1.4.0 to complete the rollback

Staged/Canary Rollout

Pushing a new version to every user at once means a hidden bug covers everyone. Staged/canary rollout prevents this. Ship to just 1% first, and watch the error rate and metrics. If nothing breaks, widen gradually to 5%, 25%, 100%. Like the canary in a coal mine, a small group of users senses the danger first. At the first sign of trouble, stop or reverse the rollout immediately.

Background Download, Apply on Restart

Stopping the user to watch a progress bar is a bad experience. A mature updater downloads quietly in the background and, once ready, only says "will be applied on the next restart." Then when the user reopens the app, it smoothly switches to the new version already fetched. Chrome, VS Code, and many macOS apps work this way.

Part 3. Integrity and Security

The update channel is an attractive target for attackers. If they can push a fake update, they can run code on every one of the user's devices. That is why integrity and security are not optional but mandatory.

Code Signing and Signature/Hash Verification

Code signing attaches a digital signature to the artifact with the developer's private key. The client verifies that signature with the public key, confirming that this file really came from that developer and was not tampered with in transit.

The iron rule is verify before applying. Check the hash and signature of the downloaded patch first, and proceed with the install only if it passes. Reverse the order (apply first, verify later) and it is already too late.

The Ideas Behind TUF

TUF (The Update Framework) is a framework designed to defend even against attacks aimed specifically at update systems. Instead of a single signing key, it uses several role-separated keys (root, targets, snapshot, timestamp) so that a single compromised key does not bring the whole thing down. Just adopting a few of its core ideas helps a lot.

HTTPS, Pinning, and Preventing Downgrades

Part 4. Ecosystem Tools

There is no need to reinvent the wheel. Every platform has mature update tooling.

Part 5. Data Migrations on Update

It is not only the code that changes to a new version. The schema of the local database or config files evolves too. This is a frequent source of accidents.

Example: A Small Update Manifest

The manifest by which a server tells a client "what the latest version is, where to get it, and what the hash is" usually looks like this.

{
  "channel": "stable",
  "latest": "1.5.0",
  "minimumSupported": "1.2.0",
  "releasedAt": "2026-07-03T09:00:00Z",
  "artifacts": [
    {
      "platform": "darwin-arm64",
      "url": "https://updates.example.com/app/1.5.0/app-arm64.zip",
      "sha256": "9f2c1b7e0a4d6f8b3c5e7a9d1f2b4c6e8a0d2f4b6c8e0a2d4f6b8c0e2a4d6f8b",
      "size": 41231884,
      "signature": "MEUCIQD...base64-signature..."
    },
    {
      "platform": "win32-x64",
      "url": "https://updates.example.com/app/1.5.0/app-x64.nupkg",
      "sha256": "1a3c5e7b9d0f2a4c6e8b0d2f4a6c8e0b2d4f6a8c0e2b4d6f8a0c2e4b6d8f0a2c",
      "size": 39882140,
      "signature": "MEQCIF...base64-signature..."
    }
  ],
  "delta": {
    "from": "1.4.0",
    "url": "https://updates.example.com/app/1.4.0-to-1.5.0.patch",
    "sha256": "7b9d1f3a5c7e9b0d2f4a6c8e0b2d4f6a8c0e2b4d6f8a0c2e4b6d8f0a2c4e6b8d"
  }
}

The client fetches this manifest over HTTPS, uses minimumSupported to block downgrades, downloads the artifact, verifies sha256 and signature, and on success swaps atomically.

Example: Service Worker Update

In a PWA, a service worker is cached like an "installed app," so you need a flow to notify the user of a new version and switch smoothly. Below is a minimal example that notifies the user when a new service worker enters the waiting state and, if they agree, activates it immediately.

// page-side code
navigator.serviceWorker.register("/sw.js").then((reg) => {
  reg.addEventListener("updatefound", () => {
    const newWorker = reg.installing;
    newWorker.addEventListener("statechange", () => {
      // new worker finished installing and an old worker still controls the page
      if (newWorker.state === "installed" && navigator.serviceWorker.controller) {
        // time to show a "new version ready" banner to the user
        showUpdateBanner(() => {
          // when the user clicks "update now," activate the waiting worker at once
          newWorker.postMessage({ type: "SKIP_WAITING" });
        });
      }
    });
  });
});

// reload once when the controller changes (= the new worker activates)
let refreshing = false;
navigator.serviceWorker.addEventListener("controllerchange", () => {
  if (refreshing) return;
  refreshing = true;
  window.location.reload();
});
// sw.js — service-worker-side code
self.addEventListener("message", (event) => {
  if (event.data && event.data.type === "SKIP_WAITING") {
    // skip the waiting state and promote this worker to the active one now
    self.skipWaiting();
  }
});

self.addEventListener("activate", (event) => {
  // take control of every open tab as soon as we activate
  event.waitUntil(self.clients.claim());
});

Decision Checklist

Questions worth asking yourself when settling on an update strategy for a new project.

Wrapping Up

The essence of an updatable solution is actually simple. Download, verify, swap atomically, and roll back on failure. On top of that, layer a considerate wizard UX (per-step validation, preview, resume), a rollout that widens safely (canary, channels), integrity that blocks attacks (code signing, TUF, downgrade prevention), and migrations that handle the evolution of data (idempotent, sequential, compatible).

Every platform already has good tools: Sparkle, Squirrel, electron-updater, Tauri, service workers, OS package managers. What matters is understanding these principles and picking the combination that fits your product's risk and scale. Installing is only the beginning; a good update experience is what keeps a product alive for the long run.

References

Comments

No comments yet.

Sign in to leave a comment