The Electron updater ships as three entry points — one for each Electron process. This page shows the complete code for all three.
Requirements
| Requirement | Value |
|---|---|
| Electron | >=28.0.0 (declared as a peer dependency) |
| Module format | ES modules — every entry point is built as ESM |
contextIsolation |
true (Electron's default) |
sandbox |
false on any window whose preload loads this package |
| Asset paths | Your build must emit relative asset paths — see below |
| External tools | None. Bundles are extracted in-process. |
The preload entry is an ES module, so Electron requires it to be named with a .mjs extension and to run unsandboxed. If you need sandbox: true, bundle the preload yourself into a CommonJS file instead.
Install
npm install @deploy-your-app/electron-update-manager
1. Main process
The main process owns the whole update lifecycle. It holds the config, talks to the server, verifies and extracts bundles, and reloads the window.
// main.mjs
import { app, BrowserWindow } from 'electron';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { readFileSync } from 'node:fs';
import { ElectronUpdateManager } from '@deploy-your-app/electron-update-manager/main';
const __dirname = dirname(fileURLToPath(import.meta.url));
// The directory holding the index.html you ship inside the app package.
const builtinBundlePath = join(__dirname, 'renderer');
/** @type {ElectronUpdateManager} */
let updater;
app.whenReady().then(async () => {
updater = new ElectronUpdateManager(
{
appId: 'com.yourcompany.yourapp',
channel: 'production',
autoUpdate: true,
publicKey: readFileSync(join(__dirname, 'dya-signing-public.pem'), 'utf-8'),
},
builtinBundlePath,
);
// Loads persisted state, registers the IPC handlers, starts the auto-update
// timer. Call it before you create the window.
await updater.initialize();
const win = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
preload: join(__dirname, 'preload.mjs'),
contextIsolation: true,
sandbox: false,
},
});
// Give the updater the window so it can activate bundles itself. Without
// this, applying an update only emits `reload` and nothing loads the files.
updater.setWindow(win);
await win.loadFile(updater.getBundlePath());
app.on('before-quit', () => updater.destroy());
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});
Two calls carry the wiring:
getBundlePath()returns theindex.htmlof the active bundle, falling back to the built-in one. Always load through it rather than hardcoding a path.setWindow(win)lets the updater callloadFile()itself on apply and on rollback. You can pass the window up front instead:await updater.initialize({ window: win }).
The second constructor argument is the built-in bundle directory. It must contain an index.html at its root — that file is what loads before any update is applied, and what the updater falls back to on reset().
Your build must use relative asset paths
This is the one requirement that will silently break an otherwise correct setup.
Bundles are served with loadFile(), over file://, from a directory under Electron's userData path. An index.html that references /assets/app.js resolves that against the root of the filesystem, not the bundle. The window loads, no error is raised, and the app renders blank — the update looks applied and is simply broken.
Configure your bundler to emit relative paths:
// vite.config.js
export default { base: './' };
// webpack.config.js
module.exports = { output: { publicPath: './' } };
The updater checks the bundle's index.html when it loads one and logs a warning to the main-process console if it finds root-relative references. The warning is not a substitute for configuring the build — it fires when the window is already about to render blank.
If your app serves its assets over a custom protocol instead of file://, do not call setWindow(). Handle the reload event and load the bundle yourself; absolute paths are fine in that case.
2. Preload script
The preload entry auto-registers on import. It exposes window.deployYourApp through contextBridge, so the renderer never touches ipcRenderer.
// preload.mjs
import '@deploy-your-app/electron-update-manager/preload';
That single import is the whole preload. The module also exports registerPreload(), but it calls that function itself at import time, so invoking it again raises the contextBridge "existing property" error.
3. Renderer
The renderer's one required job is calling notifyAppReady(). If it is not called within appReadyTimeout after a bundle is applied, the updater rolls back.
// renderer.js
import { getUpdaterApi } from '@deploy-your-app/electron-update-manager/renderer';
const api = getUpdaterApi();
// Report progress and outcomes. Each subscription returns an unsubscribe fn.
api.onUpdateAvailable(({ version, message }) => {
console.log(`Update ${version} available`, message);
});
api.onDownloadProgress(({ percent, bytesDownloaded, totalBytes }) => {
console.log(`${percent}% (${bytesDownloaded}/${totalBytes})`);
});
api.onUpdateApplied(({ version }) => {
console.log(`Applied ${version}`);
});
api.onRollback(({ from, to, reason }) => {
console.warn(`Rolled back from ${from} to ${to}: ${reason}`);
});
// Confirm the running bundle. Call this once the app has rendered and its
// core functionality works — it is what stops the automatic rollback.
window.addEventListener('DOMContentLoaded', () => {
api.notifyAppReady();
});
With autoUpdate: true (the default) that is all the renderer has to do. The main process checks, downloads, and applies on its own.
To drive the flow from the renderer instead, set autoUpdate: false in the main-process config and call the bridge directly:
import { autoUpdate } from '@deploy-your-app/electron-update-manager/renderer';
const result = await autoUpdate({
onProgress: (p) => console.log(`${p.percent}%`),
});
if (result.updated) {
console.log(`Updated to ${result.version}`);
}
autoUpdate() runs check, download, and apply in one call. For step-by-step control, see the API reference.
Deploy a bundle
Build your renderer, then deploy the output directory with the Electron target:
dya deploy --target electron --channel production
Bundles deployed with --target capacitor are never returned to an Electron client, and vice versa. Full options are in the CLI commands reference.
The archive must contain index.html at its root. The updater rejects any bundle that does not, before it is ever activated.
Signing and encryption keys
Generate key pairs once per app:
dya keys generate
dya-signing-public.pemgoes into thepublicKeyconfig option. When it is set, every downloaded bundle's RSA signature is verified before extraction. When it is unset, only the SHA-256 checksum is checked.dya-encryption-private.pemgoes into theencryptionPrivateKeyconfig option, and is only needed if you deploy encrypted bundles. It is embedded in your app binary — never place it in your web assets, since those are what get shipped over the air.
Key generation and rotation are covered in the CLI commands reference and Encryption.
Verify the wiring
- Start the app. A
dya-device-idfile, adya-settings.jsonfile, and adya-bundles/directory appear under Electron'suserDatapath. - Deploy a bundle with a visible change:
dya deploy --target electron --channel production. - With
autoUpdate: truethe first check runs about 3 seconds afterinitialize(), and then everycheckIntervalseconds (600 by default). - Watch the renderer console for
onUpdateAvailableandonDownloadProgress.
The default applyMode is whenIdle, so the downloaded bundle loads on the next launch rather than interrupting the session. Set applyMode: 'immediate' while testing to see the window reload right away.
Troubleshooting
The package is missing in a packaged build. Install it as a runtime dependency, not a dev dependency. Packagers prune devDependencies from the shipped app.
The preload bridge is undefined in the renderer. The built preload is ESM, so the file must use the .mjs extension and the window must set sandbox: false. A sandboxed preload cannot use ESM imports, and sandbox defaults to true.
Every check returns no update. Confirm the appId matches the dashboard, that you deployed with --target electron, and that the channel exists on that app.
Next steps
- API reference — every method, option, event, and IPC channel.
- Update lifecycle — how a bundle is verified, activated, and rolled back.
- SDK data collection — what the updater sends, and what you must disclose.