0014. Update architecture with a signed feed
Status
Section titled “Status”Accepted
2026-10-25
Validation
Section titled “Validation”Validated on 2026-10-25: the signed feed, the rejection rules and silent delta updates work for NSIS, AppImage and self-signed macOS builds. See the validation results.
Context
Section titled “Context”Astela updates itself on Windows, macOS and Linux across several package formats: NSIS, MSI, dmg and zip, AppImage, deb and rpm (ADR 0003). Updates have to be quiet and safe: no dialogs, no data loss, and a way back when a new version fails to start. Organizations need to switch updates off, choose a channel, pin a version prefix and point clients at an internal mirror.
electron-updater 6.8 handles download, blockmap deltas, SHA-512 checks and installation for NSIS, macOS and AppImage. Its standard providers trust an unsigned latest.yml, so whoever controls the download location controls what gets installed. It has no notion of feed expiry, rollback protection or policy-driven channels.
Binaries are large and are hosted on GitHub Releases, which has no bandwidth limit. GitHub Pages suits small files that are redeployed often.
Decision
Section titled “Decision”Astela uses electron-updater 6.8.x in the browser process as the installation engine, behind its own signed feed.
- Feed layout:
updates/v1/index.jsonon Pages lists the releases per channel with a monotonicsequence,issuedAtandexpiresAt(30 days after issue). Each version has a manifestreleases/<version>.jsonthat lists the artifacts per OS, architecture and format with file name, size, SHA-512 and blockmap, relative todownloadBase. The default base is the GitHub Releases download location of the version’s tag; theUpdateURLpolicy replaces it with a mirror. - Signatures: index and manifests carry detached Ed25519 signatures (
.sig), verified with Node’scrypto.verifywithout extra dependencies. The binary embeds two public keys, the current one and its successor, so the signing key can rotate without breaking installed clients. The successor’s private key stays offline until the rotation. - Rejection: an invalid signature, a
sequencelower than the last one seen (rollback attack), an expiredexpiresAt(freeze attack), and any downgrade that no policy allows. - Trust chain: Ed25519 from index to manifest, SHA-512 for each file, then the platform code signature where available (Authenticode on Windows, code signing on macOS).
- Provider: a
SignedFeedProviderimplements electron-updater’scustomprovider (getLatestVersion()andresolveFiles()) and returns data from verified manifests only. Format-specificIUpdateInstallerstrategies wrapNsisUpdater,MacUpdaterandAppImageUpdater. - Formats outside the in-app updater: MSI installations are detected through a marker under
HKLMand are updated by IT tooling. deb and rpm installations update through the system package manager from Astela’s own repositories (ADR 0015). The in-app updater is off for all three. - Freshness:
pages.ymlre-signs the feed weekly with a newexpiresAt; that is a deployment, not a commit (ADR 0016). With an expired feed nothing is installed, “About” andapp://diagnosticsreport the stale feed, andfreshness.ymlturns red. GitHub pauses schedules in public repositories after 60 days without activity, so a manual re-sign throughworkflow_dispatchwill be documented in WORKFLOW.md together with the release pipeline. Mirrors must synchronize at least every 14 days. There is deliberately no second feed as a release asset: it would expire and conflict with the Pages feed. - Flow without dialogs: the first check runs 30–60 s after start, outside the start-up path, then every 4 h with ±30 min jitter and only when online. Downloads run in the background with deltas. A staged update shows a small dot on the menu button, a “Restart to update” entry and a status in “About”. It installs silently on a normal quit, or during idle time with a silent restart and session restore when the update has been ready for at least 2 h, the system has been idle for at least 30 min, and no audio, video, download, WebRTC session or private window is active. A system shutdown never starts an installation; it then runs at the next start before the window opens. Afterwards a non-modal “What’s new in vX.Y” note links to the changelog.
- Channels:
stableandbeta; beta also receives stable versions. Moving from beta to stable never downgrades: the client stays on its version until stable overtakes it. - Policies:
UpdatePolicy(0 off, 1 automatic, 2 manual only),TargetChannel,TargetVersionPrefix(the highest matching version in the index),UpdateURLandAutoUpdateCheckPeriodMinutes.RollbackToTargetVersionand a maintenance window follow in M5 (enterprise). - Mirror:
tools/mirrorcopies feed and packages one to one to an internal HTTP server; signatures stay valid because artifact URLs are relative. - Rollback with the BootGuard: before an update is applied, the running version writes
boot-state.jsonwithpending,previousandattemptsand keeps a verified installer of itself locally. The first lines of the new version’s main code incrementattempts; the version counts as healthy once the shell has painted, the profile database is open and it has run for 10 s without crashing. After two failed starts, the previous version is restored from the local package without network access, the data snapshots taken before migrations are restored (ADR 0009), and the failed version is blocked locally until a newer one appears.
The full flow per format is described in updates.
Consequences
Section titled “Consequences”Positive:
- A compromised download host or mirror cannot push code, which closes the unsigned
latest.ymlgap of electron-updater. - Rollback and freeze attacks are detected, and key rotation needs no special release.
- Organizations get channel, version pin, mirror and off switch through the same policies on every OS (ADR 0012).
- Delta downloads and silent installation are expected to keep updates small and unnoticed.
- A release that fails to start is expected to roll back on its own instead of leaving users with a broken browser.
Negative:
- The feed, its verification and the BootGuard are security-critical project code; they need updater tests on every pull request (ADR 0022).
- If weekly re-signing stops, the feed expires and clients stop updating until a maintainer re-signs. This fails safe but needs attention.
- Losing the signing keys would end updates for installed clients; the successor key and an encrypted offline backup reduce that risk.
- The custom provider couples Astela to electron-updater’s provider interface, which must be rechecked on every upgrade.
- The BootGuard cannot detect crashes before the first JavaScript runs, such as a damaged binary; package smoke tests in the release pipeline cover that case.
- MSI installations get no in-app updates until M5 adds an optional update service for them.
Alternatives considered
Section titled “Alternatives considered”- electron-updater with its GitHub provider: trusts an unsigned
latest.ymland has no expiry, no rollback protection and no policy-driven channels. - Full TUF (The Update Framework): strong guarantees, but its roles and metadata are heavier than a project with a single release signer needs. The feed keeps TUF’s key ideas: signed metadata, a monotonic sequence and expiry.
- update.electronjs.org or Squirrel alone: no Linux formats, no mirrors and no enterprise policies, and an external service would sit in the update path.
- Native updaters per OS such as Sparkle or WinSparkle: three update systems to maintain and no shared feed or policy model.
- A custom downloader and installer: would duplicate the blockmap deltas and per-format installation that electron-updater already provides.
- Binaries on Pages: would exceed the size and traffic limits of Pages.