Skip to main content

Electron Example (electron-updater)

An Electron desktop app that checks for updates with the faynoSync JavaScript SDK and downloads/installs them through electron-updater using its generic provider. This maps to the electron-builder updater type, which produces *.yml feed metadata.

Repository: github.com/ku9nov/faynoSync-electron-example

Use it as a reference for adding auto updates to an Electron app with a self-hosted update server: the update check, the native download and install, and the parts electron-updater does not cover on its own (changelogs, critical updates, required intermediate versions, telemetry).

Why the SDK runs before electron-updater​

electron-updater is good at downloading, verifying and installing an update, but it polls its feed on every check and only knows what the latest*.yml file says. faynoSync puts the JavaScript SDK in front of it:

  • On the common no-update path the SDK answers from the edge cache when edgeURL is set, so neither the API nor electron-updater is involved.
  • The server decides what to offer: staged rollouts, required intermediate builds and per-channel releases are resolved before the native updater starts.
  • The SDK response carries the changelog and the critical flag, which the *.yml feed does not.

See Examples overview for how this pattern applies to the Squirrel and Tauri examples too.

Running the example​

Requirements: Node.js 18.13 or later and a running faynoSync server (see Local deployment).

git clone https://github.com/ku9nov/faynoSync-electron-example.git
cd faynoSync-electron-example
npm install
cp .env.example .env
npm start

.env.example contains:

APP_NAME=faynoSync-electron-example
VERSION=0.0.1
CHANNEL=nightly
OWNER=admin
BASE_URL=http://localhost:9000
EDGE_URL=
REPORT_KEY=
REPORT_INTERVAL_MS=60000
AUTO_DOWNLOAD=true

Configuration​

src/main/config.js resolves each setting from the environment first, then from a faynosync block that the build bakes into the packaged package.json, then from a default:

SettingEnv varDefault
App nameAPP_NAMEpackage.json name
Current versionVERSIONpackage.json version
ChannelCHANNELnightly
OwnerOWNERadmin
API URLBASE_URLhttp://localhost:9000
Edge URLEDGE_URLempty (edge disabled)
Report keyREPORT_KEYempty (reports disabled)
Background downloadAUTO_DOWNLOADtrue

A packaged app does not ship .env. electron-builder.config.js copies these variables into package.json under faynosync via extraMetadata at build time, so set them in the build environment.

Integration pattern​

  1. The SDK's checkForUpdates runs first as an edge-first gate — it decides whether an update exists (and sends telemetry) before anything native starts.
  2. If an update exists, the app derives the electron-updater feed directory from the SDK response and hands it to autoUpdater.setFeedURL.
  3. electron-updater reads latest*.yml from that directory and downloads/installs.

Creating the client​

import { Client, systemPlatform, systemArch } from '@faynosync/sdk-js';

let client;
function getClient() {
if (!client) {
client = new Client({ baseURL, edgeURL: edgeURL || undefined });
}
return client;
}

edgeURL is optional — when set, checkForUpdates tries the CDN first and falls back to the API automatically.

The edge-first update check​

const resp = await getClient().checkForUpdates({
owner,
appName: app_name,
version,
channel,
platform: systemPlatform(),
arch: systemArch(),
deviceId,
});

if (resp.updateAvailable) {
// start the background download (see below)
} else {
// already up to date
}

systemPlatform() / systemArch() return the Node.js runtime values (process.platform / process.arch); pass whatever your faynoSync platform/arch config expects.

The example checks once when the window has loaded and again whenever the user clicks "Check for updates".

Deriving the electron-updater feed directory​

electron-updater's generic provider needs the directory that contains the latest*.yml metadata, not a single file. faynoSync returns that metadata file as a yml entry in packageUrls, so the app strips the filename to get the directory:

function feedDirFromPackageUrls(packageUrls) {
const yml = (packageUrls || []).find((p) => p.package === 'yml');
if (!yml || !yml.url) return null;
return yml.url.substring(0, yml.url.lastIndexOf('/'));
}

Then it configures electron-updater and starts the download:

import { autoUpdater } from 'electron-updater';

const feedUrl = feedDirFromPackageUrls(resp.packageUrls);
if (feedUrl) {
autoUpdater.setFeedURL({ provider: 'generic', url: feedUrl });
autoUpdater.checkForUpdates();
}
Why derive the directory instead of using updateUrl

The SDK's checkForUpdates returns direct artifact URLs (updateUrl, packageUrls) for the manual flow. electron-updater doesn't consume those directly — it wants the folder holding latest.yml / latest-mac.yml. The yml package URL points at that metadata file, so its parent directory is the feed URL. This keeps a single faynoSync response usable by both a manual "pick a package" UI and the automatic electron-updater flow.

Because electron-builder uploads are stored per version (/updater/<app>/<channel>/<platform>/<arch>/<version>/), the derived directory always belongs to the version faynoSync chose. If the server offers a required intermediate build instead of the latest release, electron-updater downloads that intermediate.

Keep the faynoSync display fields

The electron-updater *.yml feed only describes the artifact — it does not carry faynoSync's changelog, critical, or isIntermediateRequired flags. That's why the app pulls only the yml URL out of packageUrls to locate the feed directory, but keeps reading those display fields from the SDK's checkForUpdates response. If you pointed electron-updater at the feed directly and dropped the SDK call, you'd lose the changelog, the critical-update styling, and the intermediate-update signal in the UI.

const meta = {
changelog: resp.changelog,
critical: resp.critical,
isIntermediateRequired: resp.isIntermediateRequired,
source: resp.source,
};

Download and install​

src/main/autoUpdate.js sets two electron-updater options:

autoUpdater.autoDownload = autoDownload;
autoUpdater.autoInstallOnAppQuit = true;
  • With AUTO_DOWNLOAD=true the update downloads silently in the background and the window shows the changelog and progress.
  • With AUTO_DOWNLOAD=false the app asks "Download / Later" on update-available and calls autoUpdater.downloadUpdate() only on confirmation.

The rest of the lifecycle is forwarded to the renderer over IPC:

electron-updater eventWhat the app does
download-progressSends percent, bytes and speed to the update modal.
update-downloadedShows the restart button; clicking it calls autoUpdater.quitAndInstall().
update-not-availableShows an error: the feed has no newer version than the running app.
errorShows the error and offers a manual download list built from packageUrls.

If the user does not restart, autoInstallOnAppQuit installs the downloaded update on the next quit.

electron-updater only runs in a packaged app; in dev mode the example falls back to a manual download list built from packageUrls. See src/main/updater.js and src/main/autoUpdate.js for the full flow.

Publishing a release​

Build the app with one of the package scripts:

npm run dist:mac     # dmg + zip
npm run dist:win # NSIS installer + zip
npm run dist:linux # AppImage + deb

Then upload the build output together with its *.yml file and --updater electron-builder, so faynoSync stores it in the isolated per-version folder electron-updater reads from. For macOS on Apple Silicon with the faynoSync CLI:

faynosync upload \
--app faynoSync-electron-example \
--version 0.0.2 \
--channel nightly \
--platform darwin \
--arch arm64 \
--updater electron-builder \
--file dist/latest-mac.yml \
--file dist/FaynoSync-0.0.2-arm64-mac.zip \
--file dist/FaynoSync-0.0.2-arm64-mac.zip.blockmap \
--file dist/FaynoSync-0.0.2-arm64.dmg \
--publish \
--changelog "Fixed the settings dialog"

The platform and arch names must match what the app sends: the example sends Node.js values (darwin, win32, linux / x64, arm64), so create those platforms and architectures or map the values in the app. The electron-builder updater page lists the files to upload per OS.

Add --critical to mark the release critical, --intermediate to make it a required intermediate build, or upload with a rollout percentage for a staged rollout.

Telemetry and reports​

The example creates a random device ID on first start and stores it in the app's userData folder, so it stays the same across restarts. It passes it as deviceId on every check. That ID drives:

  • Telemetry: version, platform and active-device statistics on the dashboard. On edge hits the SDK reports through /telemetry/beacon, since the API is never called.
  • Staged rollouts: the device's rollout bucket is computed from the same ID.

When REPORT_KEY is set, the example also sends randomly generated failure reports with the SDK's reportEvent every REPORT_INTERVAL_MS, to demonstrate rollout health reports. In a real app, send a report only when an update or install actually fails.

Troubleshooting​

"No matching update found in feed metadata." faynoSync reported an update, but the version in latest*.yml is not newer than the running app. The SDK check uses VERSION (or the baked faynosync.version), while electron-updater compares against app.getVersion() from package.json. Keep both the same.

No background download, only the manual list. Either the app is not packaged (dev mode), or the release has no yml entry in packageUrls. Upload the latest*.yml file with --updater electron-builder.

The update installs on Windows/Linux but not on macOS. macOS updates are installed from the .zip, not the .dmg, and require a signed app. Check that the .zip was uploaded and see the code signing section of the Electron auto update tutorial.

No update found right after publishing with EDGE_URL set. A CDN in front of the edge bucket can serve the previous response for up to a minute after a publish. See Edge and S3 Response Cache.