Split View: 업데이트 가능한 솔루션 만드는 법: 설치 마법사부터 안전한 자동 업데이트까지
업데이트 가능한 솔루션 만드는 법: 설치 마법사부터 안전한 자동 업데이트까지
- 들어가며 — 설치는 시작일 뿐이다
- 1부. 설치 마법사의 UX
- 2부. 업데이트 메커니즘
- 3부. 무결성과 보안
- 4부. 생태계 도구들
- 5부. 업데이트 시 데이터 마이그레이션
- 예제: 작은 업데이트 매니페스트
- 예제: 서비스 워커 업데이트
- 결정 체크리스트
- 마치며
- 참고 자료
들어가며 — 설치는 시작일 뿐이다
소프트웨어를 사용자에게 전달하는 순간부터 진짜 문제가 시작됩니다. 처음 설치는 한 번뿐이지만, 업데이트는 제품이 살아 있는 내내 계속됩니다. 버그를 고치고, 기능을 더하고, 보안 구멍을 막으려면 이미 배포된 수천, 수만 대의 설치본을 안전하게 새 버전으로 갈아 끼워야 합니다.
이 글은 두 가지를 함께 다룹니다. 하나는 사용자가 처음 만나는 **설치 마법사(wizard)**의 UX 설계이고, 다른 하나는 그 뒤에서 조용히 돌아가는 자동 업데이트 메커니즘입니다. 데스크톱 앱이든 모바일 앱이든 웹 PWA든, "업데이트 가능한 솔루션"을 만드는 원리는 놀랄 만큼 비슷합니다. 다운로드하고, 검증하고, 원자적으로 교체하고, 실패하면 되돌린다. 이 네 단계가 전부입니다.
1부. 설치 마법사의 UX
멀티스텝 흐름과 단계별 검증
마법사는 복잡한 설정을 여러 단계로 쪼갠 UI입니다. 한 화면에 모든 옵션을 쏟아붓는 대신, 사용자를 한 걸음씩 안내합니다. 좋은 마법사에는 공통된 뼈대가 있습니다.
- 단계별 검증(per-step validation): 다음으로 넘어가기 전에 그 단계의 입력을 즉시 검증합니다. 마지막에 한꺼번에 실패를 터뜨리지 않습니다. 예를 들어 설치 경로를 고르는 단계라면, 그 경로에 쓰기 권한이 있는지·용량이 충분한지를 그 자리에서 확인합니다.
- 진행 표시와 뒤로 가기(progress and back): 지금 몇 단계 중 몇 번째인지 보여주고, 이전 단계로 자유롭게 돌아갈 수 있어야 합니다. 되돌아가도 이미 입력한 값이 사라지지 않아야 합니다.
- 합리적 기본값(sensible defaults): 대부분의 사용자는 기본값 그대로 "다음"만 누릅니다. 그러니 기본값이 곧 대다수의 실제 설정이 됩니다. 기본값은 가장 안전하고 흔한 선택이어야 합니다.
미리보기, 드라이런, 그리고 실패 시 롤백
설치나 업데이트는 시스템을 바꾸는 작업입니다. 사용자가 "적용"을 누르기 전에 무엇이 바뀌는지 보여주면 신뢰가 올라갑니다.
- 드라이런/미리보기(dry-run/preview): 실제로 파일을 건드리기 전에, 어떤 파일이 추가·수정·삭제될지 요약해 보여줍니다. 서버 설정 마법사라면 "이 설정으로 연결을 시도해 본" 결과를 먼저 보여줄 수 있습니다.
- 실패 시 롤백(rollback on failure): 설치 도중 중간에 실패하면, 어중간하게 반쯤 설치된 상태로 남겨선 안 됩니다. 트랜잭션처럼, 전부 성공하거나 전부 원래대로 되돌아가야 합니다.
재개 가능성
큰 파일을 내려받는 도중 네트워크가 끊기거나, 사용자가 실수로 창을 닫을 수 있습니다. 그때마다 처음부터 다시 하게 만들면 사용자는 지칩니다. **재개 가능성(resumability)**은 중단된 지점의 상태를 저장해 두었다가, 다시 열었을 때 그 지점부터 이어가는 능력입니다. 다운로드는 HTTP Range 요청으로 이어받고, 마법사의 입력값은 로컬에 임시 저장해 둡니다.
2부. 업데이트 메커니즘
시맨틱 버저닝과 업데이트 채널
업데이트를 관리하려면 먼저 버전을 체계적으로 매겨야 합니다. **시맨틱 버저닝(SemVer)**은 버전을 MAJOR.MINOR.PATCH 세 자리로 나눕니다. 호환성이 깨지면 MAJOR, 기능이 추가되면 MINOR, 버그만 고치면 PATCH를 올립니다. 클라이언트는 이 규칙을 보고 "이 업데이트가 안전한 자잘한 수정인지, 조심해야 할 큰 변경인지"를 판단할 수 있습니다.
여기에 **업데이트 채널(update channel)**을 더합니다. 같은 제품이라도 stable(안정), beta(베타), nightly(매일 빌드)처럼 여러 흐름을 두어, 위험을 감수할 수 있는 사용자에게만 먼저 새 버전을 흘려보냅니다. 대부분의 사용자는 stable에 두고, 얼리어답터는 beta로 옮깁니다.
전체 업데이트 vs 델타/차분 업데이트
가장 단순한 방법은 새 버전 전체를 통째로 내려받아 갈아 끼우는 **전체 업데이트(full update)**입니다. 구현이 쉽지만, 앱이 수백 MB라면 한 줄만 고쳐도 매번 수백 MB를 내려받아야 합니다.
**델타/차분 업데이트(delta/differential update)**는 이 낭비를 없앱니다. 이전 버전과 새 버전의 **차이(diff)**만 내려받아 로컬에서 합칩니다. 대표적인 도구가 있습니다.
- bsdiff: 바이너리 두 개의 차이를 작은 패치로 만들어 냅니다. 널리 쓰이는 고전적 방식입니다.
- Courgette: 크롬이 만든 방식으로, 실행 파일을 어셈블리 수준에서 분석해 bsdiff보다 훨씬 작은 패치를 만듭니다. 재컴파일로 주소가 통째로 밀려도 실제 변경은 작다는 점을 이용합니다.
델타 업데이트는 대역폭을 크게 아끼지만, "이전 버전이 정확히 그 버전이어야" 패치가 맞습니다. 그래서 보통 여러 버전에서 최신으로 가는 여러 개의 패치를 준비하거나, 델타가 실패하면 전체 업데이트로 자동 폴백하도록 만듭니다.
원자적 교체와 롤백
업데이트에서 가장 위험한 순간은 파일을 실제로 갈아 끼우는 때입니다. 옛 파일을 지우고 새 파일을 쓰는 중간에 정전이 나면? **원자적 교체(atomic swap)**가 이 문제를 막습니다.
핵심은 "완전히 준비된 뒤 마지막에 딱 한 번 전환한다"는 것입니다. 새 버전을 옆에 통째로 설치해 두고(예: v2 폴더), 검증이 끝나면 "현재 버전"을 가리키는 포인터(심볼릭 링크나 설정)를 v1에서 v2로 한 번에 바꿉니다. 파일 시스템의 원자적 rename을 쓰면, 어느 순간에도 사용자는 온전한 v1 아니면 온전한 v2를 봅니다. 반쯤 섞인 상태는 존재하지 않습니다. 문제가 생기면 포인터를 v1로 되돌리기만 하면 **롤백(rollback)**이 끝납니다.
/app/current -> /app/versions/1.4.0 (원자적 rename 으로 전환)
/app/versions/1.5.0 <- 미리 설치 + 검증 완료
전환 후:
/app/current -> /app/versions/1.5.0
실패 시 current 포인터를 1.4.0 으로 되돌리면 롤백 완료
스테이지드/카나리 롤아웃
새 버전을 모든 사용자에게 동시에 밀면, 숨어 있던 버그가 전체를 덮칩니다. **스테이지드/카나리 롤아웃(staged/canary rollout)**은 이를 막습니다. 먼저 1%에게만 배포하고, 오류율과 지표를 관찰합니다. 문제가 없으면 5%, 25%, 100%로 서서히 넓힙니다. 광산의 카나리아처럼, 소수의 사용자가 먼저 위험을 감지해 주는 셈입니다. 이상이 보이면 즉시 롤아웃을 멈추거나 되돌립니다.
백그라운드 다운로드, 재시작 시 적용
사용자를 멈춰 세우고 진행 막대를 지켜보게 하는 건 나쁜 경험입니다. 성숙한 업데이터는 백그라운드에서 조용히 내려받고(background download), 준비가 끝나면 "다음에 재시작할 때 적용됩니다"라고만 알립니다. 그리고 사용자가 앱을 다시 켤 때 이미 받아 둔 새 버전으로 매끄럽게 전환합니다. 크롬, VS Code, macOS의 여러 앱이 이 방식을 씁니다.
3부. 무결성과 보안
업데이트 채널은 공격자에게 매력적인 표적입니다. 만약 가짜 업데이트를 밀어 넣을 수 있다면, 공격자는 사용자의 모든 기기에서 코드를 실행할 수 있습니다. 그래서 무결성과 보안은 선택이 아니라 필수입니다.
코드 서명과 서명·해시 검증
**코드 서명(code signing)**은 개발자의 비밀 키로 배포물에 디지털 서명을 붙이는 것입니다. 클라이언트는 공개 키로 그 서명을 검증해, 이 파일이 진짜 그 개발자에게서 왔고 도중에 변조되지 않았음을 확인합니다.
철칙은 **적용하기 전에 검증(verify before applying)**입니다. 내려받은 패치의 해시와 서명을 먼저 확인하고, 통과한 경우에만 설치를 진행합니다. 순서가 뒤바뀌면(먼저 적용하고 나중에 검증) 이미 늦습니다.
TUF의 아이디어
**TUF(The Update Framework)**는 업데이트 시스템을 겨냥한 특수한 공격까지 방어하도록 설계된 프레임워크입니다. 단일 서명 키 하나가 아니라 역할을 나눈 여러 키(루트, 타깃, 스냅샷, 타임스탬프)를 두고, 키 하나가 털려도 전체가 무너지지 않게 합니다. 핵심 아이디어 몇 가지만 챙겨도 큰 도움이 됩니다.
- 역할 분리와 키 회전: 서명 권한을 나누고, 키가 노출되면 갈아 끼울 수 있게 합니다.
- 신선도 보장(freshness): 타임스탬프 메타데이터로 "지금 받은 이 메타데이터가 최신인지"를 확인해, 오래된 정보를 새 것처럼 속이는 공격을 막습니다.
HTTPS, 핀닝, 다운그레이드 방지
- HTTPS와 인증서 핀닝(pinning): 업데이트는 반드시 HTTPS로 받고, 중요한 경우 서버 인증서를 앱에 고정(pinning)해 중간자 공격을 어렵게 만듭니다.
- 다운그레이드 공격(downgrade attack) 방지: 공격자는 서명이 유효한 옛 취약 버전을 다시 밀어 알려진 구멍을 되살리려 합니다. 이를 막으려면 클라이언트가 현재 버전보다 낮은 버전으로는 절대 내려가지 않게 하고, 서버가 제시한 버전 번호가 롤백된 것은 아닌지 확인해야 합니다.
4부. 생태계 도구들
바퀴를 새로 발명할 필요는 없습니다. 플랫폼마다 성숙한 업데이트 도구가 있습니다.
- Squirrel: 윈도우와 macOS용 업데이트 프레임워크입니다. 델타 업데이트와 조용한 백그라운드 설치에 강합니다.
- Sparkle: macOS 앱의 사실상 표준 업데이터입니다. appcast라는 RSS/XML 피드로 새 버전 정보를 알리고, 서명 검증을 내장합니다.
- electron-updater: 일렉트론 앱의 자동 업데이트를 담당하며, 내부적으로 Squirrel과 appcast 스타일 피드를 함께 씁니다.
- Tauri updater: 러스트 기반 Tauri 앱의 공식 업데이터로, 서명 검증과 업데이트 매니페스트를 기본 제공합니다.
- OS 패키지 매니저: 리눅스라면 apt, dnf 같은 시스템 패키지 매니저가 업데이트·의존성·서명 검증을 통째로 맡아 줍니다. 앱이 직접 업데이터를 만들 필요가 줄어듭니다.
- 모바일 인앱 업데이트(in-app updates): 안드로이드의 In-App Updates API처럼, 스토어를 거치되 앱 안에서 유연/즉시 업데이트를 유도할 수 있습니다.
- 웹/PWA 서비스 워커: 웹은 새로고침이 곧 업데이트지만, PWA는 서비스 워커의 업데이트 수명주기를 통해 "새 버전 준비됨 → 다음 방문에 활성화"를 세밀하게 제어합니다. 아래에서 예제로 다룹니다.
- 서버 주도 기능 플래그/원격 설정(remote config): 코드를 새로 배포하지 않고도, 서버가 내려주는 플래그로 기능을 켜고 끄거나 점진 노출할 수 있습니다. 롤아웃과 즉시 롤백의 또 다른 축입니다.
5부. 업데이트 시 데이터 마이그레이션
코드만 새 버전으로 바뀌는 게 아닙니다. 로컬 데이터베이스나 설정 파일의 **형식(schema)**도 함께 진화합니다. 여기서 자주 사고가 납니다.
- 스키마/버전 마이그레이션: 데이터에 버전 번호를 붙이고, 앱이 켜질 때 "현재 데이터 버전 → 목표 버전"으로 순서대로 마이그레이션 스크립트를 적용합니다. 마이그레이션은 반드시 순차적이고 누적적이어야 합니다.
- 멱등한 마이그레이션(idempotent migration): 같은 마이그레이션을 두 번 돌려도 결과가 같아야 합니다. 중간에 실패하고 재시작하는 상황이 흔하기 때문입니다. "컬럼이 없으면 추가"처럼 조건을 확인하고 실행합니다.
- 전방/후방 호환성(forward/backward compatibility): 롤백을 대비해, 새 버전이 쓴 데이터를 옛 버전이 (완벽하진 않아도) 깨지지 않고 읽을 수 있으면 좋습니다. 새 필드는 옵셔널로 추가하고, 필드를 곧바로 지우기보다 한동안 무시하는 편이 안전합니다.
예제: 작은 업데이트 매니페스트
서버가 클라이언트에게 "최신 버전이 무엇이고, 어디서 받고, 해시는 무엇인지"를 알려주는 매니페스트는 대개 이런 모습입니다.
{
"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"
}
}
클라이언트는 이 매니페스트를 HTTPS로 받아, minimumSupported로 다운그레이드를 막고, 아티팩트를 내려받은 뒤 sha256과 signature를 검증하고, 통과하면 원자적으로 교체합니다.
예제: 서비스 워커 업데이트
PWA에서 서비스 워커는 "설치된 앱"처럼 캐시되므로, 새 버전을 사용자에게 알리고 매끄럽게 전환하는 흐름이 필요합니다. 아래는 새 서비스 워커가 대기 상태가 되면 사용자에게 알리고, 동의하면 즉시 활성화하는 최소 예제입니다.
// 페이지 쪽 코드
navigator.serviceWorker.register("/sw.js").then((reg) => {
reg.addEventListener("updatefound", () => {
const newWorker = reg.installing;
newWorker.addEventListener("statechange", () => {
// 새 워커가 설치를 마치고, 기존 워커가 아직 페이지를 제어 중이면
if (newWorker.state === "installed" && navigator.serviceWorker.controller) {
// 사용자에게 "새 버전이 준비되었습니다" 배너를 보여줄 시점
showUpdateBanner(() => {
// 사용자가 "지금 갱신"을 누르면 대기 중 워커를 즉시 활성화
newWorker.postMessage({ type: "SKIP_WAITING" });
});
}
});
});
});
// 컨트롤러가 바뀌면(=새 워커가 활성화되면) 한 번만 새로고침
let refreshing = false;
navigator.serviceWorker.addEventListener("controllerchange", () => {
if (refreshing) return;
refreshing = true;
window.location.reload();
});
// sw.js — 서비스 워커 쪽 코드
self.addEventListener("message", (event) => {
if (event.data && event.data.type === "SKIP_WAITING") {
// 대기 상태를 건너뛰고 이 워커를 즉시 활성 워커로 승격
self.skipWaiting();
}
});
self.addEventListener("activate", (event) => {
// 활성화되는 즉시 열려 있는 모든 탭의 제어권을 가져온다
event.waitUntil(self.clients.claim());
});
결정 체크리스트
새 프로젝트에서 업데이트 전략을 정할 때 스스로에게 던지면 좋은 질문들입니다.
- 전달 경로: 스토어/패키지 매니저를 통하는가, 자체 업데이터를 두는가? 가능하면 플랫폼 표준(Sparkle, Squirrel, apt/dnf, 스토어 인앱 업데이트)을 먼저 검토합니다.
- 업데이트 크기: 델타 업데이트가 필요할 만큼 앱이 큰가? 델타를 쓴다면 전체 업데이트 폴백을 반드시 함께 둡니다.
- 적용 안전성: 원자적 교체와 롤백이 준비되어 있는가? 반쯤 설치된 상태가 절대 남지 않는가?
- 무결성: 적용 전에 서명과 해시를 검증하는가? 다운그레이드를 막는가? HTTPS인가?
- 롤아웃: 카나리로 소수에게 먼저 내보내고 지표를 보는가? 한 번에 롤백할 스위치가 있는가?
- 채널: stable/beta 채널을 나눌 것인가? 기능 플래그로 코드 배포 없이 껐다 켤 수 있는가?
- 데이터: 스키마 마이그레이션이 멱등하고 순차적인가? 롤백 시 옛 버전이 새 데이터를 견디는가?
- 경험: 백그라운드 다운로드 후 재시작 시 적용하는가? 마법사에 단계별 검증·뒤로 가기·재개가 있는가?
마치며
업데이트 가능한 솔루션의 본질은 사실 단순합니다. 다운로드하고, 검증하고, 원자적으로 교체하고, 실패하면 되돌린다. 여기에 사용자를 배려하는 마법사 UX(단계별 검증, 미리보기, 재개), 안전하게 넓히는 롤아웃(카나리, 채널), 공격을 막는 무결성(코드 서명, TUF, 다운그레이드 방지), 그리고 데이터의 진화를 다루는 마이그레이션(멱등·순차·호환성)을 얹으면 됩니다.
플랫폼마다 좋은 도구가 이미 있습니다. Sparkle, Squirrel, electron-updater, Tauri, 서비스 워커, OS 패키지 매니저. 중요한 것은 이 원리들을 이해하고, 내 제품의 위험과 규모에 맞는 조합을 고르는 일입니다. 설치는 시작일 뿐이고, 좋은 업데이트 경험이야말로 제품을 오래 살아 있게 합니다.
참고 자료
- The Update Framework (TUF): https://theupdateframework.io/
- Sparkle (macOS 업데이트 프레임워크): https://sparkle-project.org/
- Squirrel.Windows: https://github.com/Squirrel/Squirrel.Windows
- electron-updater 문서: https://www.electron.build/auto-update
- Tauri Updater 가이드: https://v2.tauri.app/plugin/updater/
- Chromium Courgette 소개: https://www.chromium.org/developers/design-documents/software-updates-courgette/
- Semantic Versioning: https://semver.org/
- MDN 서비스 워커 수명주기: https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers
Building Updatable Solutions: From Installer Wizards to Safe Auto-Updates
- Introduction — Installing Is Only the Beginning
- Part 1. Wizard UX
- Part 2. Update Mechanisms
- Part 3. Integrity and Security
- Part 4. Ecosystem Tools
- Part 5. Data Migrations on Update
- Example: A Small Update Manifest
- Example: Service Worker Update
- Decision Checklist
- Wrapping Up
- References
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