Skip to content

필사 모드: Building Updatable Solutions: From Installer Wizards to Safe Auto-Updates

English
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

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.

- **Per-step validation**: Validate each step's input immediately, before moving on. Do not save up all the failures for the end. If a step picks the install path, check right there that the path is writable and has enough space.

- **Progress and back**: Show which step of how many the user is on, and let them freely return to earlier steps. Going back must not erase values already entered.

- **Sensible defaults**: Most users just keep the defaults and hit "Next." So the defaults become the actual configuration for the majority. Defaults should be the safest, most common choice.

Preview, Dry-Run, and Rollback on Failure

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

- **Dry-run/preview**: Before touching any files, summarize which files will be added, modified, or removed. A server-setup wizard can first show the result of "trying the connection with this configuration."

- **Rollback on failure**: If an install fails partway through, it must not leave a half-installed mess. Like a transaction, it should either fully succeed or fully revert to the original state.

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.

- **bsdiff**: Produces a small patch from the difference between two binaries. A widely used classic approach.

- **Courgette**: Chrome's approach, which analyzes the executable at the assembly level to produce a much smaller patch than bsdiff. It exploits the fact that recompilation can shift addresses wholesale even when the real change is tiny.

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.

- **Role separation and key rotation**: Divide signing authority, and make keys replaceable if exposed.

- **Freshness guarantees**: Use timestamp metadata to confirm that the metadata you just received is current, blocking attacks that pass off stale information as new.

HTTPS, Pinning, and Preventing Downgrades

- **HTTPS and certificate pinning**: Always fetch updates over HTTPS, and where it matters, pin the server certificate into the app to make man-in-the-middle attacks harder.

- **Preventing downgrade attacks**: An attacker may re-push an **old, vulnerable version** whose signature is still valid, reviving a known hole. To prevent this, the client must never go below its current version, and should confirm that the version number the server offers has not been rolled back.

Part 4. Ecosystem Tools

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

- **Squirrel**: An update framework for Windows and macOS. Strong on delta updates and quiet background installs.

- **Sparkle**: The de facto standard updater for macOS apps. It announces new versions through an RSS/XML feed called an **appcast** and has signature verification built in.

- **electron-updater**: Handles auto-update for Electron apps, using Squirrel and appcast-style feeds under the hood.

- **Tauri updater**: The official updater for Rust-based Tauri apps, providing signature verification and an update manifest out of the box.

- **OS package managers**: On Linux, system package managers like apt and dnf take over updates, dependencies, and signature verification wholesale, so apps need to build their own updater far less often.

- **Mobile in-app updates**: Like Android's In-App Updates API, you can prompt flexible or immediate updates from within the app while still going through the store.

- **Web/PWA service workers**: On the web a refresh is the update, but a PWA finely controls "new version ready, then activate on next visit" through the service worker update lifecycle. We cover this with an example below.

- **Server-driven feature flags/remote config**: Without shipping new code, a server-delivered flag can turn features on and off or roll them out gradually. Another axis for rollout and instant rollback.

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.

- **Schema/version migrations**: Attach a version number to the data, and on startup apply migration scripts in order from "current data version to target version." Migrations must be sequential and cumulative.

- **Idempotent migrations**: Running the same migration twice must yield the same result, because failing partway and restarting is common. Check a condition and then act, as in "add the column if it does not exist."

- **Forward/backward compatibility**: To prepare for rollback, it helps if the old version can read data written by the new version without breaking, even if not perfectly. Add new fields as optional, and prefer ignoring a field for a while over deleting it outright.

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.

- **Delivery path**: Through a store/package manager, or your own updater? Where possible, consider platform standards first (Sparkle, Squirrel, apt/dnf, store in-app updates).

- **Update size**: Is the app big enough to need delta updates? If you use delta, always keep a full-update fallback.

- **Apply safety**: Are atomic swap and rollback in place? Is a half-installed state ever left behind?

- **Integrity**: Do you verify signature and hash before applying? Do you block downgrades? Is it HTTPS?

- **Rollout**: Do you canary to a small group first and watch metrics? Is there a single switch to roll back?

- **Channels**: Will you split stable/beta channels? Can feature flags toggle things without shipping code?

- **Data**: Are schema migrations idempotent and sequential? Does the old version survive the new data on rollback?

- **Experience**: Do you download in the background and apply on restart? Does the wizard have per-step validation, back, and resume?

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

- The Update Framework (TUF): https://theupdateframework.io/

- Sparkle (macOS update framework): https://sparkle-project.org/

- Squirrel.Windows: https://github.com/Squirrel/Squirrel.Windows

- electron-updater docs: https://www.electron.build/auto-update

- Tauri Updater guide: https://v2.tauri.app/plugin/updater/

- Chromium Courgette overview: https://www.chromium.org/developers/design-documents/software-updates-courgette/

- Semantic Versioning: https://semver.org/

- MDN Service Worker lifecycle: https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers

현재 단락 (1/130)

The real problems start the moment you ship software to a user. The first install happens once, but ...

작성 글자: 0원문 글자: 13,124작성 단락: 0/130