Update architecture
Status: partly implemented. Signed updates work for NSIS, AppImage and macOS, with rollback for NSIS and AppImage, and the package repositories for deb and rpm are built and tested; the release pipeline publishes every release. See implementation status.
Astela is designed to update itself on every supported platform without dialogs. A signed feed decides what gets installed. Depending on the package format, electron-updater, the operating system’s package manager or the IT department decides how it gets installed. The decision records are ADR 0014 (update architecture with a signed feed) and ADR 0015 (Linux package repositories); see the decision record index.
Overview
Section titled “Overview”flowchart LR REL["GitHub Release: packages, blockmaps, SHA256SUMS"] --> PAGES PAGES["GitHub Pages feed: updates/v1/index.json and releases/{version}.json, each with .sig"] --> SRC MIR[("Organization mirror, set by the UpdateURL policy")] --> SRC SRC["IManifestSource"] --> VER["Ed25519Verifier with public keys built into the binary"] VER --> POL["Update policy: off, automatic or manual; channel; version pin"] POL --> EU["electron-updater in main: download, delta, SHA-512"] EU --> INST["IUpdateInstaller per package format"] INST --> BG["BootGuard and rollback"]The update service belongs to the updates core module, which is always active (see modules). It runs in the browser process; the overview describes the process model.
Chain of trust
Section titled “Chain of trust”Every link in the chain verifies the next one:
- The Ed25519 signature of the index, which references the per-version manifests.
- The Ed25519 signature of the manifest, which lists the SHA-512 hash of every file.
- The SHA-512 hash of the downloaded file.
- Authenticode on Windows (once releases are signed) or the code signature on macOS.
This chain also closes a known weakness of electron-updater 6: its own latest.yml metadata is not signed (as of October 2026).
Signed feed
Section titled “Signed feed”The feed is a deliberately small design inspired by The Update Framework (TUF). It is designed to protect against tampering, rollback and freeze attacks without additional infrastructure.
updates/v1/index.json lists the releases per channel. Besides the release entries, it carries three fields that protect clients against stale or replayed data:
| Field | Meaning |
|---|---|
sequence |
Monotonic counter. A client never accepts a value lower than the last one it saw. |
issuedAt |
Time of signing. |
expiresAt |
issuedAt plus 30 days. |
Per-version manifests
Section titled “Per-version manifests”Each version has a manifest, releases/<version>.json, with its own signature. It lists the artifacts per operating system, architecture and package format with file name, size, SHA-512 hash and blockmap. All file references are relative to downloadBase.
- The default
downloadBaseishttps://github.com/Secoolioo/astela/releases/download/v<version>/. - The
UpdateURLpolicy points the client at a mirror instead. Because all references are relative, the signatures stay valid on the mirror.
Signatures and keys
Section titled “Signatures and keys”- The index and every manifest have a detached Ed25519 signature (
.sig). - Verification uses Node’s built-in
crypto.verifyin the main process. There is no extra dependency, and a verification takes only microseconds. - The binary embeds two public keys: the current key and its successor, so that keys can rotate without breaking installed clients.
- The private keys are stored only as GitHub environment secrets and in an encrypted offline backup kept by the repository owner. The rotation procedure will be documented with the release pipeline in the maintainer workflow.
Rejection rules
Section titled “Rejection rules”The client will reject a feed or manifest when:
- the signature is invalid,
sequenceis lower than the last value seen (rollback attack),expiresAtlies in the past (freeze attack),- the update would be a downgrade and no policy allows it.
Freshness and expiry
Section titled “Freshness and expiry”- The
pages.ymlworkflow will re-sign the feed every week with a freshexpiresAt. Re-signing is a deployment, not a commit. - An expired feed fails safe: nothing will be installed. The status “Feed out of date” will appear on the About page and on
app://diagnostics, andverify-livefails when the published feed is close to its expiry. - GitHub pauses scheduled workflows in public repositories after 60 days without activity (as of October 2026). The maintainer workflow will document how to re-sign manually with
workflow_dispatch. - Mirrors must synchronize at least every 14 days.
Hosting
Section titled “Hosting”- Binaries will be downloaded from GitHub Releases, which has no bandwidth limits (as of October 2026).
- The small feed lives on GitHub Pages.
- There is deliberately no second, immutable feed as a release asset. It would expire and would conflict with the feed on Pages.
Update engine
Section titled “Update engine”Astela uses electron-updater 6.8.x as the installation engine. It runs in the main process with asynchronous I/O and a custom provider (provider: 'custom'):
- The
SignedFeedProviderclass will implementgetLatestVersion()andresolveFiles()and will only return data taken from verified manifests. - electron-updater will handle the download, the blockmap delta, the SHA-512 check and the installation for the formats it supports.
The update domain is built from these interfaces:
| Interface | Responsibility |
|---|---|
IUpdateService |
check(), download(), installOnQuit() and the current state |
IManifestSource |
fetch(channel) returns the signed manifest from GitHub Pages or a mirror |
ISignatureVerifier |
verify(bytes, signature) checks Ed25519 signatures against the embedded keys |
IUpdateInstaller |
one strategy per package format: canHandle(installType), stage(artifact), apply() |
BootGuard |
beginBoot(), markHealthy() and needsRollback() for the first starts after an update |
The updater is a state machine: idle → checking → downloading → staged → applying.
Install flow without dialogs
Section titled “Install flow without dialogs”- Check: 30–60 seconds after start, outside the startup path, then every 4 hours with ±30 minutes of jitter, and only while online.
- Download: in the background, as a delta where the format allows it, with full verification.
- Notify: a subtle dot on the menu button, a “Restart to update” menu entry and the status on the About page.
- Install:
- silently when the user quits normally, or
- during idle time with a silent restart and session restore, when all of these hold: the update has been ready for at least 2 hours, the system has been idle for at least 30 minutes, and no audio or video is playing, no download is running, no WebRTC session is active and no private window is open.
- If the system is shutting down, no installation starts. It runs at the next start instead, before the first window opens.
- Afterwards: a non-modal “What’s new in vX.Y” note with a link to the changelog.
Channels
Section titled “Channels”- There are two channels:
stableandbeta. The beta channel also receives stable versions. - Switching from beta to stable never downgrades. The installation will stay on its version until a stable release overtakes it.
- Prereleases are versioned
X.Y.Z-beta.N. The maintainer workflow defines the versioning rules, and the roadmap lists the milestones.
Update policies
Section titled “Update policies”Organizations will control updates with these policies (see enterprise and settings, policies and themes):
| Policy | Effect |
|---|---|
UpdatePolicy |
0 = off, 1 = automatic, 2 = manual only |
TargetChannel |
Selects the channel. |
TargetVersionPrefix |
Selects the highest version in the index that matches the prefix. |
UpdateURL |
Points the client at a mirror. |
AutoUpdateCheckPeriodMinutes |
Sets the check interval. |
UpdateURLis a sensitive policy. It will be honored only from machine-level policy (HKLMon Windows, forced preferences on macOS); otherwiseapp://diagnosticswill show a warning.- From M5,
RollbackToTargetVersionand an update maintenance window follow.
Organization mirror
Section titled “Organization mirror”tools/mirror is designed to mirror the feed and the packages one to one onto an internal HTTP server. The signatures stay valid because all URLs in the feed are relative. A mirror must synchronize at least every 14 days.
Rollback
Section titled “Rollback”Updates must never destroy data. Every data migration takes a snapshot first, and an older version never opens a newer schema for writing (see data). On top of that, the BootGuard is designed to roll back a version that fails to start:
- Before an update is applied, the old, working version will write
boot-state.jsonwith{ pending: vNew, previous: vOld, attempts: 0 }. At that point the installation package of the running version will be available locally, either kept from the last update or downloaded and verified in the background beforehand. - The first lines of the new version’s main process will increment
attempts. The new version counts as healthy once the shell is drawn, the profile database is open and it has run for 10 seconds without crashing. - After 2 failed starts, the
BootGuardwill:- restore the previous version silently from the local package, without network access,
- restore the data snapshots taken before the migration,
- block the failed version locally until a newer version appears.
- If a version fails to start without a preceding update, Astela will start in safe mode instead (manually in M1, automatically from M2).
Documented limit: the BootGuard cannot detect crashes that happen before the first JavaScript runs, for example with a damaged binary. The package smoke tests in every release run are the safeguard against this (see testing).
Strategy per package format
Section titled “Strategy per package format”| Format | First install | Update | Delta | Silent | Rollback |
|---|---|---|---|---|---|
| Windows NSIS (per user, default for home users; x64, arm64) | astela-windows-<arch>-setup.exe, no administrator rights |
electron-updater NsisUpdater through the signed feed |
yes (blockmap) | yes, without a UAC prompt | reinstall the previous version from its verified installer |
| Windows MSI (per machine, organizations; x64; stable only) | msiexec /i … /qn with ALLUSERS=1 and a fixed UpgradeCode |
managed by IT; the in-app updater is off | no | yes, through IT tooling | by IT, with ALLOWDOWNGRADE |
| macOS (dmg to install, zip to update; separate x64 and arm64 builds) | open the dmg and drag the app to Applications | electron-updater MacUpdater (Squirrel.Mac); needs a stable code signature, self-signed for now |
yes (zip blockmap; the first update after a dmg install is full) | yes | APFS clone of the current bundle, swapped back by a helper |
| Linux AppImage (x86_64, arm64) | download, mark as executable; static runtime, no libfuse2 needed | electron-updater AppImageUpdater replaces the file in place |
yes (blockmap) | yes | .previous copy, swapped back immediately |
| Linux deb (amd64, arm64) | apt install ./astela-linux-<arch>.deb |
Astela APT repository, together with system updates; in-app updater off | no | yes, with system updates | apt install astela=<previous>; the repository keeps 2 versions |
| Linux rpm (x86_64, aarch64) | dnf install with the downloaded .rpm file |
Astela DNF repository, together with system updates | no | yes | dnf downgrade |
Windows NSIS
Section titled “Windows NSIS”- NSIS is the default for home users: a per-user installation without administrator rights, for x64 and arm64.
- Before it installs or uninstalls, the installer closes a running browser of the current user with
tasklistandtaskkill(app/build/installer.nsh). electron-builder’s own check starts PowerShell and queries WMI; on fresh Windows runners that hung for up to 30 minutes, and companies often restrict PowerShell. nsiswith a separate blockmap is used. In the validation run, an update of a 113 MB installer downloaded 0.78 MB, sonsis-webis not needed.- Windows installers ship unsigned for now, so the first installation triggers a SmartScreen warning; updates run without it. The solution is Windows code signing (see code signing).
Windows MSI
Section titled “Windows MSI”- The MSI is a native, per-machine package for organizations. It is built for x64 and for stable releases only, because MSI ignores prerelease identifiers.
- The installation type is recorded outside the policy keys, in
HKLM\SOFTWARE\Astela\InstallwithType=msi. Astela will detect this marker and turn the in-app updater off. IT distributes new MSI packages (Group Policy, Intune, SCCM) and controls updates withUpdatePolicy. - electron-updater cannot install MSI packages (as of October 2026). An optional elevated update task running as
SYSTEM, which installs verified MSI packages silently in the way Chrome and Edge update, is planned for M5. - For arm64, electron-builder produces an MSI marked as x64 (as of October 2026). A dedicated WiX template follows in M5; until then, arm64 devices use NSIS.
- Silent installation flags, properties and Intune detection are described in enterprise.
- x64 and arm64 get separate builds. The dmg is for the first installation; the zip is for updates.
- If the app runs from somewhere other than Applications (App Translocation), Astela will show a notice.
- Squirrel.Mac requires a stable code signature across versions.
- With an Apple Developer ID, the app is signed and notarized, and updates will install silently.
- Without an Apple Developer ID, there are two paths:
- A self-signed certificate with a stable designated requirement, so that Squirrel.Mac and the Keychain keep working across updates. A CI updater test on macOS verified this path. The first start then needs a one-time “Open Anyway” in System Settings (macOS Sequoia and later).
- Fallback, not needed after the validation: the update is downloaded and verified in the background and installed with one click from the menu.
- Current decision: there is no code signing budget for now, so macOS uses path 1. The Apple Developer Program (99 USD per year, as of October 2026) would add notarization and remove the Gatekeeper hurdle; once its secrets exist, the release workflow will sign and notarize automatically.
Linux AppImage
Section titled “Linux AppImage”- The AppImage uses the static runtime, so libfuse2 is not required.
- Ubuntu 24.04 and later restrict unprivileged user namespaces, which disables the Chromium sandbox for AppImages (as of October 2026).
- Astela will never fall back to
--no-sandboxsilently. The launcher that electron-builder generates for AppImages does exactly that (validation results), so Astela’s AppImage ships its own launcher. It starts the browser with the sandbox; only if Chromium refuses to start for lack of a sandbox does it open a local page with a one-click fix and the recommendation to use the deb or rpm package, in a restricted mode without web content. - The fix will use
pkexecto install an AppArmor profile withuserns. Because AppImages run from/tmp/.mount_*, the profile applies to/tmp/.mount_*/astela; this scope will be documented for users. - “Continue without sandbox” will be available only after explicit confirmation and will keep a permanent warning bar.
- The fix is tested on a host runner with the user namespace restriction enabled, not in a container. The validation run confirmed on Ubuntu 24.04 (x64 and arm64) that the profile gives the AppImage a working namespace sandbox.
Linux deb
Section titled “Linux deb”- Astela will publish its own APT repository on GitHub Pages with a signed
InReleasefile and two suites:stableandbeta. postinstsets up the repository source once (deb822 format withSigned-By), on the beta channel for a beta package and on the stable channel otherwise. An environment variable allows opting out, another one points the source at a mirror.- Beta packages are versioned
0.1.0~beta.N, so they sort correctly before0.1.0. - Updates will arrive with the regular system updates. The in-app updater is turned off through
package-type. - electron-updater’s deb updater asks for a password and does not verify a GPG signature (as of October 2026), so it is not used.
- The deb package will enable the sandbox through an AppArmor profile with
userns(electron-builder template), with the SUID helper as a fallback. - GitHub Pages has a 1 GB size limit and a soft limit of 100 GB of traffic per month (as of October 2026). The repository will therefore keep only the last 2 versions; if it outgrows that, the package pool moves to other hosting.
Linux rpm
Section titled “Linux rpm”- Astela will publish its own DNF repository. The metadata will live on GitHub Pages; the packages will be served directly from GitHub Releases through
createrepo_c --baseurl(xml:base, supported by dnf5). - The metadata is signed (
repomd.xml.asc) and the packages are signed withrpmsign. %postcreates/etc/yum.repos.d/astela.repoonce; a beta package (0.1.0~beta.N) points it at the beta channel, which also receives stable releases. The build writes the channel into the package, because rpm cannot query its database while it installs a package.- On an rpm upgrade, the removal script of the old package runs after the install script of the new one. It therefore removes the command link, the AppArmor profile and the repository only when the package is removed for good. Packages up to
0.1.0-beta.2still removed them on an upgrade; a%posttransscript, which runs after all others, sets the command link and the profile up again. Thelinux-reposjob checks that the command survives the upgrades between its test packages and that the package scripts print nothing; the live update proof checks the command after the upgrade from the previous release. - Fedora and RHEL will be tested; openSUSE is supported on a best-effort basis.
Upgrade order
Section titled “Upgrade order”The upgrade sequence 0.1.0-beta.1 → 0.1.0-beta.2 → 0.1.0 must work for NSIS, AppImage, deb (0.1.0~beta.N) and rpm. MSI packages are built only for stable versions. The linux-repos CI job tests this order for deb and rpm with test versions, and after each release, the live update proof tests the step from the previous release on real installations (see testing).
Versionless asset names
Section titled “Versionless asset names”Release assets carry no version in their names, for example astela-windows-x64-setup.exe, astela-macos-arm64.dmg and astela-linux-x86_64.AppImage. They will be attached to versioned tags, so releases/latest/download/<name> stays a stable link. The blockmap delta will find the previous version through its tag.
Validation spikes
Section titled “Validation spikes”These spikes ran at the start of M1, before the first walking skeleton release, because they carry the highest risk. The validation results record what was measured:
- Custom provider with Ed25519 verification, with NSIS deltas on Windows and AppImage deltas on Linux; delta sizes are measured.
- macOS with a self-signed certificate: Squirrel.Mac updates and Keychain stability.
- AppImage sandbox fix on Ubuntu 24.04 and 26.04, on a host runner with the user namespace restriction enabled.
- APT and DNF repositories: installation, upgrade and the version order
~beta.N→ stable. Containers test the package logic; the host tests the sandbox. node:sqlitein the utility process with FTS5, WAL andVACUUM INTOon three operating systems and two architectures.- Inventory of the web platform APIs exposed by the embedded Chromium in the packaged build (see security).
- Moving a tab between windows without a reload on Windows, macOS, X11 and Wayland.
Code signing
Section titled “Code signing”Code signing is designed into the release workflows from the start. For now there is no code signing budget: macOS releases use the self-signed certificate path described below, and Windows releases ship unsigned. Signing steps will run only when the corresponding secrets exist; without certificates, everything will be built unsigned. Once the secrets are added, releases will be signed automatically.
macOS signing
Section titled “macOS signing”- Without an Apple Developer ID, a self-signed code signing certificate valid for 10 years keeps the designated requirement stable. The repository owner generates it locally with
tools/owner/setup.sh. - Secrets for a Developer ID:
CSC_LINKandCSC_KEY_PASSWORD: the Developer ID Application certificate as.p12.APPLE_API_KEY,APPLE_API_KEY_IDandAPPLE_API_ISSUER: an App Store Connect team key.
- Switching from the self-signed certificate to a Developer ID breaks the designated requirement once. Existing installations will therefore receive that one update through a guided one-click step.
- Signing, notarization and stapling of the app and the dmg will run automatically.
- The Hardened Runtime uses minimal entitlements: JIT, camera and microphone, with usage descriptions.
Windows signing
Section titled “Windows signing”For now, Windows packages will ship unsigned. The release workflow is designed to support three options and to sign automatically as soon as the secret for one of them exists; the secret that is present selects the option:
- SignPath Foundation: free for open-source projects after an application; the certificate is issued to “SignPath Foundation”.
- The repository owner approves each release with one click in the SignPath interface. The release job will wait up to 24 hours for that approval.
- Betas may be published unsigned. Fully automatic signing, including within the 24-hour target for critical security fixes, requires option 2 or 3.
- Azure Artifact Signing: about 9.99 USD per month; for individuals only in the USA and Canada, for organizations also in the EU (as of October 2026).
- Astela’s own OV certificate in a cloud HSM.
With every option, publisherName is fixed, and the updater will check it fail-closed.
Linux signing
Section titled “Linux signing”- A GPG key signs the APT and DNF repositories.
- The Ed25519 update key signs the feed and
SHA256SUMS.
See security for release integrity and the supply chain measures.
Testing
Section titled “Testing”The updater tests are designed to use a local test feed on 127.0.0.1 with two builds (X-test.1 → X-test.2) and a test key. The mirror is set through the real policy mechanism, so production builds need no test backdoor. Negative cases cover a wrong signature, a hash mismatch, a downgrade and an expired manifest; a rollback case starts a build with a deliberate boot failure. After every release, an installation of the previous release proves the real path: in its normal configuration, it updates itself to the new release through the live feed, and its deb and rpm packages upgrade through the live repositories. Details are in testing.
Implementation status
Section titled “Implementation status”Every package format and architecture in the table above is built and smoke-tested on every pull request (app/electron-builder.config.mjs), with the versionless asset names. The packages are unsigned so far.
The update path for NSIS, AppImage and macOS is implemented:
-
Signed feed (
app/src/domain/updates/feed.ts): index and manifests are verified before they are parsed, with the rejection rules above (signature, sequence, expiry, no downgrade), the channel and the version prefix. -
Keys: the public keys are read at build time from
app/build/keys/update-*.pub.pem. A build without keys cannot verify the feed and therefore never updates. The release keys are committed; SECURITY.md lists their fingerprints. -
Engine (
app/src/platform/updates/): theSignedFeedProviderfor electron-updater. A check reads the signed feed first and loads electron-updater only when it finds an update, so the checks every 4 hours cost one small download and a signature check. Downloads use the blockmap delta and the SHA-512 from the signed manifest. -
Service (
app/src/domain/updates/update-service.ts): the first check 30–60 seconds after start, then every 4 hours with ±30 minutes of jitter, a retry while offline, and the installation while the browser exits after a normal quit. -
Policies:
UpdatePolicy,TargetChannel,TargetVersionPrefix,UpdateURL(machine-level only) andAutoUpdateCheckPeriodMinutes. -
Installation types: MSI (a per-machine installation under Program Files), deb and rpm turn the in-app updater off. The registry marker described above replaces the path check when the MSI properties land.
-
Feed tool (
tools/feed/feed.ts): writes the manifest of a release from electron-builder’s update information of all builds, signs the manifests of all published releases with a fresh index, signs files such asSHA256SUMS, and serves a feed locally for tests. -
Release pipeline (
release.yml,pages.yml): attaches the manifest to every release asupdate-manifest.json. Every Pages deployment signs all manifests again with a new index whose sequence is the current Unix time and which is valid for 30 days; the weekly run renews it. See ADR 0016. -
Boot guard (
app/src/domain/updates/boot-guard.ts,app/src/main/boot.ts): the working version records an update before it installs it; the new version counts its starts right after the single-instance lock and counts as healthy after 10 seconds with a loaded shell or a normal quit. At the third start after two failed ones, the previous version comes back and the failed one is blocked. The state lives in the per-userastela-updaterfolder, so it belongs to the installation, not to a profile. -
Rollback (
app/src/platform/updates/rollback.ts): an AppImage keeps a copy of itself and swaps it back; an NSIS installation fetches the installer of the running version after an update was downloaded, verifies it against the signed manifest and reinstalls it silently. -
Tests: unit tests for versions, the feed, the service and the boot guard; the
updaterCI job installs build 1 on Windows (NSIS) and Linux (AppImage), checks that it refuses a tampered feed, lets it download build 2 from a valid feed through theUpdateURLpolicy, quits it and starts build 2. Build 2 then installs build 3, which fails to start on purpose; after two failed starts, the third start must bring build 2 back. Only that test build contains the failure; it is compiled out of every other build. After each release, thelive-updatejobs ofrelease.ymlinstall the previous release from its release assets on Windows (NSIS) and Linux (AppImage), without any policy; it must find the new release in the live feed, download it and install it when it quits. Its deb and rpm packages must upgrade to the new release through the live repositories. macOS is not part of this proof yet (see testing). -
Package repositories (
tools/linux-repo/repo.ts): signs the rpm packages and builds the APT repository (one shared pool,stableandbetasuites, signedInRelease) and the DNF repositories (metadata pointing at the release downloads, signedrepomd.xml). Each channel lists its last two releases; beta also lists stable ones. -
Package scripts (
app/build/linux/): after installation, the deb or rpm package registers the repository of its channel once, with the key it ships;ASTELA_NO_REPO=1skips this andASTELA_REPO_URLpoints at a mirror. Removing the package removes the repository again. Packages ship the repository key fromapp/build/keys/linux-repo.pub.asc. -
Repository test: the
linux-reposCI job installs0.0.1-beta.1from its file on Ubuntu 22.04 and 24.04, Debian 12 and 13 and the two latest Fedora releases, upgrades to0.0.1-beta.2and to0.0.1through the repositories, and removes the package. The Ubuntu containers use the package mirror of the runner, and the job tries again when a mirror that is being updated cannot serve its lists or packages yet.
Not implemented yet: the rollback on macOS, restoring data snapshots (a rollback does not bring back the profile.db.v<version>.bak copy that a migration of the profile database writes), the update indicator, the “Restart to update” entry and the idle installation, the status on the About and diagnostics pages, the “What’s new” note, the mirror tool, the macOS updater test, and code signing for Windows and with an Apple Developer ID.