Contents

Automatic Updates

How to setup everything needed to enable automatic updates in your application.

MōBrowser checking for updates

MōBrowser provides a built-in mechanism to check for updates, download the latest version of your application, and install it. This feature is useful when you want to distribute updates to your users without them having to manually download and install the new version of your application.

Enabling automatic updates in your application consists of several steps including:

  1. Configuring an update server.
  2. Adding update functionality to your application.
  3. Building the application and generating the update files.
  4. Uploading the update files to the update server.
  5. Securing your updates so that only you can publish them.

More details about each step are provided in the sections below.

Note: On Windows, the application can be auto-updated only if it’s installed using the native installer generated by MōBrowser. If you distribute your application in any other way (e.g., by manually copying the executable files or as a ZIP archive), the auto-update functionality will not work.

Configuring an update server 

First, you need to set up an update server that will host the new versions of your application. The update server is an HTTPS server that hosts the files and allows everyone to download them using a direct link. You can use a third-party service like Amazon S3 or Google Cloud Storage as an update server.

MōBrowser requires the update feed to be served over HTTPS — plain http:// feeds are rejected, with the sole exception of http://localhost for local testing. Serving updates over HTTPS prevents a network attacker from tampering with the packages in transit.

Once you set up the update server, remember its URL. You will need it in the next step.

Checking for updates in the app 

The automatic update functionality in MōBrowser is very flexible. You decide how exactly your application should check for updates, download and install them.

  1. You can check for updates in the background. When a new version of your application is available, you can download and install it automatically and ask the user to restart the application to apply the update.
  2. You can check for updates at the application startup, download and install them, and programmatically restart the application to make sure the user launches the latest version of your application.
  3. You can check for updates on demand when user selects “Check for Updates…” from the application menu or clicks a button in the Settings dialog. If a new version of your application is available, you can ask the user if they want to download and install it.

The following sections demonstrate how to check for updates, download and install them, retrieve update information, dismiss specific updates, and monitor installation progress.

Checking for updates 

To check for updates, you need to know the update server URL that you should pass to the checkForUpdate() method as shown below:

import { app } from '@mobrowser/api';

const result = await app.checkForUpdate('https://app.com/updates')

The checkForUpdate() method returns a Promise that resolves to one of the following:

  • An AppUpdate object if an update is available
  • undefined if no update is available
  • A string containing an error message if an error occurs while checking for updates (e.g., no internet connection, update server is unreachable, or update files are missing on the server)
import { app, AppUpdate } from '@mobrowser/api';

const result = await app.checkForUpdate('https://app.com/updates')
if (!result) {
  // No update is available or do nothing.
} else if (typeof result === 'string') {
  // An error occurred while checking for updates.
} else {
  const appUpdate: AppUpdate = result
  console.log('Update available: ', appUpdate.version)
}

Downloading and dismissing update 

If an update is available, you can download it by calling the download() method of the AppUpdate object. To apply the update, you need to restart or quit the application after it is downloaded:

const downloadResult = await appUpdate.download()
if (!downloadResult.success) {
  console.error(`Update download failed: ${downloadResult.error}`)
}

Or you can dismiss the update if you don’t want to download it:

appUpdate.dismiss()

Dismissing an update means that you don’t want to download it at this time. This could be because the user chose not to install it, the update contains known issues, or for any other reason you determine. When you check for updates again later, the dismissed update will still be available for download.

Note: Each AppUpdate instance can only be downloaded or dismissed once. Subsequent calls to download() or dismiss() on the same instance will be no-op.

Download progress 

Downloading an update may take some time depending on your internet connection speed and the size of the update.

You can get notifications about the download progress by subscribing to the progressChanged event:

appUpdate.on('progressChanged', (progress: number) => {
  console.log(`Downloading update: ${progress}%`)
})
const downloadResult = await appUpdate.download()

Restarting the application 

After the update is downloaded, you can ask the user to restart the application to apply the update or wait for the user to restart the application manually.

const downloadResult = await appUpdate.download()
if (downloadResult.success) {
  const result = await app.showMessageDialog({
    parentWindow: win,
    title: `A new version ${appUpdate.version} is available!`,
    message: 'Restart to apply the update?',
    buttons: [
      { label: 'Restart', type: 'primary' },
      { label: 'Cancel', type: 'secondary' }
    ]
  })
  if (result.button.type === 'primary') {
    app.restart()
  }
}

To restart the application programmatically, use the application restart functionality.

Generating the update files 

Now, you are ready to build your application with the automatic update functionality enabled and generate the update files. Run the following command:

npm run build && npm run pack

This command will build the application, package it into a native executable, create a native installer, and generate the update files you need to upload to the update server.

The automatic update functionality is supported on macOS and Windows only.

On Windows, you will find the update files in /build/dist/win-x64/pack/:

build/
|-- dist/
|   `-- win-x64/
|   |   `-- pack/
|   |   |   `-- <PACKAGE_ID>-<APP_VERSION>-full.nuget
|   |   |   `-- releases.win.json

On macOS, you will find the update files in /build/dist/mac-arm64/pack/:

build/
|-- dist/
|   `-- mac-arm64/
|   |   `-- pack/
|   |   |   `-- <BUNDLE_ID>-<APP_VERSION>-osx-full.nuget
|   |   |   `-- releases.osx.json

Delta updates 

You will most likely want to use the delta updates, which allow your users to download only the changes between versions instead of the entire application. It’s significantly faster to download and install the delta update than the full update.

To generate the delta update files, specify the --releases option that points to the directory containing the previous releases:

npm run pack -- --releases=path/to/previous-releases

The framework will generate the files for the full update (releases.<platform>.json and *-full.nuget) in the given directory as usual.

If the directory contains the full update files from the previous releases, the framework will automatically generate the *-delta.nupkg package for delta updates, and extend the existing releases.<platform>.json file with the new release information.

Uploading the update files 

You need to manually upload the following files to the update server:

  • *-full.nupkg — a full NuGet package for the new version of your application. This package is used when the user installs the update from scratch or when no previous version is available locally.
  • *-delta.nupkg — a delta NuGet package that contains only the differences between the previous and the new version of your application. This package is significantly smaller than the full package and allows faster updates. Delta packages are generated automatically when the --releases directory contains full .nupkg files from previous releases.
  • releases.<platform>.json — this file contains the information about the available updates for Windows. It’s a JSON file with the list of the available updates, including both full and delta packages. You need to update this file on your update server every time you release a new version of your application. This file should have the same location as NuGet packages on your update server.

Make sure the uploaded files on your update server are publicly accessible, so that your desktop application can download them.

Protecting the update files 

Auto-updates are also an attack surface: your app installs whatever the feed serves, so it’s only as trustworthy as that feed. MōBrowser locks this down, so that users can only install updates from your update server.

Why protection matters 

The application updates used to be trusted on nothing more than a checksum from the same manifest that delivered them. Whoever controls the feed controls both the package and the checksum it’s checked against. A compromised bucket or a man-in-the-middle over plain HTTP could push a malicious “update” to all your users, and nothing would catch it.

How MōBrowser protects the update files 

These things guard the pipeline:

  • HTTPS-only feed. Feeds must be served over HTTPS (except http://localhost for local dev), so nobody can tamper with packages in transit.
  • SHA-256 verification at apply time. Packages are checked with SHA-256 on the exact bytes being installed to detect corruption/tampering and ensure integrity of the package being applied.
  • Downgrade protection. An update whose version is older than the one installed is refused, and the version is bound into the signature so it can’t be relabeled — an attacker can’t roll you back to an older, known-vulnerable release.
  • Two trust anchors you control. An update installs only if it passes one of the two checks below.
    • it carries a valid Ed25519 signature, or
    • it is code-signed with the same identity as the installed app.

If neither check passes, the update is rejected.

The two trust anchors 

Ed25519 package signature. You sign every release with a private key. The matching public key ships inside the installed app, and the app checks each update against it. No private key means no valid signature, so an attacker can’t forge one the app will accept.

Code-signing with the same identity. The update has to be code-signed with the same identity as the installed app. MōBrowser reads that identity from the installed app and checks the update against it:

  • On Windows, the update needs a valid Authenticode signature from a trusted CA whose certificate subject matches the installed app.
  • On macOS, the update must be Developer ID-signed with the same Team ID.

Why two? They fail independently, so you can recover from losing either one:

  • The signature gives you strong, publisher-only authenticity.
  • The identity anchor means losing or rotating your Ed25519 key won’t strand your users — they still accept an update code-signed by the same publisher, then pick up the new key from it.
  • The catch: an attacker only has to beat one anchor, so you’re only as strong as the weaker one. That’s fine here — your code-signing identity already signs your installer, so it’s not a new secret to guard.

A code-signed app is also what secures delta updates: each delta is itself Ed25519-signed and verified as it downloads, but the auto-update mechanism reassembles and re-compresses the full package locally, so the rebuilt package no longer matches the signed release’s hash — the update that actually gets applied is therefore authenticated by the code-signing identity anchor rather than by the Ed25519 signature.

Enabling package signing 

You never handle key material directly — the MōBrowser CLI manages the signing key for you.

Generate the key once per application (or per publisher):

npm run mobrowser signkey

This generates the signing key if none exists and prints its public key. It is idempotent — running it again won’t overwrite an existing key. The private key is stored in your operating system’s secret store (macOS Keychain / Windows Credential Manager), falling back to ~/.mobrowser/signing.key.

The signkey command supports the following forms:

CommandWhat it does
npm run mobrowser signkeyGenerates the private key if absent, then prints the public key. Idempotent.
npm run mobrowser signkey --printPrints the public key of the existing key. Errors if no key exists.
npm run mobrowser signkey --export <file>Writes the private key to <file> (for use as a CI secret).

Once a signing key is available, npm run pack signs the update packages automatically — no extra flags are needed, and the public key is embedded in the build. If no key is found, packing prints a non-blocking warning that your auto-update packages will be unsigned, and continues.

Signing in CI/CD 

The same key has to sign every release, so it needs to persist across builds. Keep it as a single CI/CD secret instead of generating one per build.

  1. Export the private key once, locally:

    npm run mobrowser signkey --export signkey.txt
    
  2. Store the contents of signkey.txt as a masked/secret CI variable named CI_MOBROWSER_AUTOUPDATE_SIGNING_KEY, then delete the local file.

  3. In the CI job that runs your build/pack, make sure that variable is present in the environment. MōBrowser’s packer inherits it and signs every update package automatically, embedding the public key. No extra flags are required.

  4. Wire your code-signing credentials into the same job so that shipped artifacts also carry the identity anchor. See Signing Application.

Key management and rotation 

  • Keep the private signing key secret. If you lose it, code-signed apps can still recover: ship updates signed by the code-signing identity anchor, and clients pick up a new signing key from the first one.
  • When you need to rotate, change only one anchor at a time:
    • Rotate the signing key: ship a release signed with the new key but code-signed with your existing identity. Clients that don’t yet trust the new key accept the update via the identity anchor, then adopt the new key.
    • Change the code-signing identity (e.g. a new Team ID): ship a release with the new identity but signed with your existing key. Clients verify via the signature and adopt the new identity.

Important: Never change both anchors in the same release. Clients could then validate neither and would need to install an application again with a new installer to recover.