OpenWork Electron Updater Mechanism: How Auto-Updates Work in the Desktop App
OpenWork uses electron-updater with a custom IPC bridge to check, download, and install signed updates while respecting organizational policies and supporting multiple release channels.
The OpenWork desktop application, built with Electron, implements a sophisticated auto-update system through the electron-updater library. This mechanism enables silent update checks, channel-based releases (stable and alpha), and enterprise policy enforcement. The implementation spans both renderer and main processes, with clear separation of concerns between UI state management and low-level updater operations.
Architecture Overview: Two-Part Update Flow
The update system splits responsibility across the renderer and main process. This design keeps the UI responsive while handling the heavy lifting of network requests and file operations in the main thread.
| Component | Responsibility | Source File |
|---|---|---|
| Renderer bridge | Exposes window.__OPENWORK_ELECTRON__.updater with methods for check, download, install, and channel management |
[electron-updater-state.ts](https://github.com/different-ai/openwork/blob/dev/apps/app/src/react-app/domains/settings/state/electron-updater-state.ts#L31-L38) |
| Main-process updater | Wires electron-updater, builds feed URLs, persists channels, handles macOS Squirrel quirks, streams progress | updater.mjs |
Renderer-Side: The useElectronUpdaterState Hook
The React-facing API lives in apps/app/src/react-app/domains/settings/state/electron-updater-state.ts. This hook abstracts all IPC communication into a TypeScript-friendly interface.
Bridge Detection and Environment Checks
At lines 100-106, the module detects whether it runs inside the Electron shell:
return window.__OPENWORK_ELECTRON__?.updater ?? null;
If the bridge is absent, the hook sets ELECTRON_UPDATER_UNSUPPORTED_REASON and degrades gracefully.
High-Level API Surface
The hook returns these key properties and methods:
appVersion– Current OpenWork version fromapp.getVersion()updateEnv–{supported: boolean, reason?: string}updateStatus– State machine:idle|checking|available|downloading|ready|errorcheckForUpdates()– Triggers manual or scheduled update checksdownloadUpdate()– Starts download with progress callbacksinstallUpdateAndRestart()– Quits and installs the pending updatesetReleaseChannel(channel)– Switches between"stable"and"alpha"
All actions forward to bridge methods: bridge.check(), bridge.download(), bridge.installAndRestart().
Auto-Check Scheduling
At lines 271-284, the hook implements intelligent re-checking:
// Builds a key from policy channel + app version
const checkKey = `${policyReleaseChannel}-${appVersion}`;
// Re-runs when the key changes (channel switch or new install)
useEffect(() => {
if (shouldScheduleElectronUpdateAutoCheck(updateAutoCheck, checkKey)) {
runCheckForUpdates();
}
}, [checkKey, updateAutoCheck]);
Main-Process: registerUpdaterIpc Implementation
The apps/desktop/electron/updater.mjs module exports registerUpdaterIpc, which sets up IPC handlers that the renderer invokes.
Channel Persistence
User-selected channels survive app restarts via JSON storage:
const CHANNEL_FILE = "electron-updater-channel.v1.json";
function readElectronUpdaterChannel(app) {
const filePath = path.join(app.getPath("userData"), CHANNEL_FILE);
// Returns { channel: "stable" | "alpha" } or default
}
function writeElectronUpdaterChannel(app, channel) {
// Persists selection to user-data folder
}
Feed URL Configuration
The updater pulls from GitHub Releases with channel-specific endpoints defined at lines 109-146:
const ELECTRON_UPDATER_FEEDS = {
stable: "https://github.com/different-ai/openwork/releases/latest/download",
alpha: "https://github.com/different-ai/openwork/releases/download/alpha-macos-latest",
};
The applyElectronUpdaterFeed function configures the autoUpdater instance:
function applyElectronUpdaterFeed(autoUpdater, channel, appVersion) {
const feedUrl = ELECTRON_UPDATER_FEEDS[channel];
autoUpdater.setFeedURL(feedUrl);
autoUpdater.allowPrerelease = (channel === "alpha");
autoUpdater.allowDowngrade = false;
return { channel, feedUrl, currentVersion: appVersion };
}
Progress Event Streaming
Download progress flows from main to renderer via IPC at lines 70-84:
autoUpdater.on('download-progress', (progressObj) => {
const payload = {
bytesPerSecond: progressObj.bytesPerSecond,
percent: progressObj.percent,
transferred: progressObj.transferred,
total: progressObj.total
};
sendToRenderer('openwork:updater:download-progress', payload);
});
macOS Squirrel Stability Fixes
Before any install operation, the module applies workarounds for common Squirrel.Mac failures:
enableSquirrelDirectContentsWrite()– Prevents bundle-move failurescleanStaleUpdaterState()– Removes stale ShipIt lock files
IPC Handler Registration
At lines 221-259, handlers bind to specific channels:
ipcMain.handle("openwork:updater:getChannel", async () => { /* ... */ });
ipcMain.handle("openwork:updater:setChannel", async (_, channel) => { /* ... */ });
ipcMain.handle("openwork:updater:check", async (_, channel, targetVersion) => { /* ... */ });
ipcMain.handle("openwork:updater:download", async () => { /* ... */ });
ipcMain.handle("openwork:updater:installAndRestart", async () => { /* ... */ });
Organizational Policy Enforcement
The version-gate.ts module (at apps/app/src/app/lib/version-gate.ts) prevents unauthorized updates in managed environments.
resolveDesktopUpdateChannel– Forces"stable"if alpha is disallowed by org policyisUpdateAllowedByDesktopConfig– Validates againstallowedDesktopVersionslistisAlphaUpdateAllowed– Enforces the "one-patch-ahead" rule for alpha builds
These helpers run inside the renderer hook before any network request, ensuring policy compliance without main-process involvement.
Build Configuration
The electron-builder.yml embeds updater metadata into packaged artifacts:
# apps/desktop/electron-builder.yml
extends: ./electron-builder.base.yml
appId: com.differentai.openwork
productName: OpenWork
protocols:
- name: OpenWork
schemes:
- openwork
artifactName: openwork-${os}-${arch}-${version}.${ext}
The autoUpdater reads the current version via app.getVersion(), falling back to package.json in development.
Complete Update Flow
- App startup –
registerUpdaterIpcinitializes handlers; renderer receives bridge object - Channel resolution – UI loads persisted channel, applies org policy via
resolveDesktopUpdateChannel - Auto-check trigger – Scheduled check calls
bridge.check(), which runsautoUpdater.checkForUpdates() - Update available – Hook transitions to
updateStatus.state = "available"with version details - Download –
downloadUpdate()registers progress handler, streams bytes, reachesstate = "ready" - Install –
installUpdateAndRestart()invokesautoUpdater.quitAndInstall()with macOS fixes applied
Using the Update API in Components
Settings Panel Integration
import { useElectronUpdaterState } from '@/domains/settings/state/electron-updater-state';
export function UpdatePanel() {
const {
appVersion,
updateStatus,
checkForUpdates,
downloadUpdate,
installUpdateAndRestart,
setReleaseChannel,
} = useElectronUpdaterState({
releaseChannel: 'stable',
onReleaseChannelChange: setReleaseChannel,
updateAutoCheck: true,
updateAutoDownload: false,
desktopConfig: null,
refreshDesktopConfig: async () => null,
setError: console.error,
});
return (
<div>
<p>Current version: {appVersion ?? '…'}</p>
{updateStatus?.state === 'idle' && (
<button onClick={() => checkForUpdates()}>Check for updates</button>
)}
{updateStatus?.state === 'available' && (
<>
<p>Version {updateStatus.version} is available.</p>
<button onClick={() => downloadUpdate()}>Download</button>
</>
)}
{updateStatus?.state === 'ready' && (
<button onClick={installUpdateAndRestart}>Install & Restart</button>
)}
{updateStatus?.state === 'error' && (
<p style={{ color: 'red' }}>{updateStatus.message}</p>
)}
</div>
);
}
Direct Bridge Access (Advanced)
const bridge = window.__OPENWORK_ELECTRON__?.updater;
if (bridge) {
// Switch to alpha channel
await bridge.setChannel('alpha');
// Check for specific version
const info = await bridge.check('alpha', '1.2.3');
console.log('Update info:', info);
}
Main Process Setup
import { app, ipcMain } from 'electron';
import { registerUpdaterIpc } from './updater.mjs';
app.whenReady().then(() => {
registerUpdaterIpc({
app,
ipcMain,
getMainWindow: () => mainWindow,
manifestChannel: 'latest',
});
});
Summary
- electron-updater handles the core update logic, with GitHub Releases hosting the artifacts
- Renderer-main IPC bridge (
window.__OPENWORK_ELECTRON__.updater) separates UI from platform code - Dual-channel support (stable/alpha) with per-channel feed URLs and persistence
- Org policy gating via
version-gate.tsprevents unauthorized updates in managed deployments - macOS-specific hardening avoids common Squirrel.Mac installation failures
- Progress streaming gives users real-time download feedback
Frequently Asked Questions
How does OpenWork support both stable and alpha release channels?
The updater maintains two static feed URLs in ELECTRON_UPDATER_FEEDS. When a user selects a channel via setReleaseChannel, the main process persists the choice to electron-updater-channel.v1.json and reconfigures autoUpdater.setFeedURL() with the appropriate endpoint. The allowPrerelease flag aligns with the selected channel.
What prevents unauthorized updates in enterprise deployments?
The version-gate.ts module enforces organizational policies before any update check occurs. Functions like resolveDesktopUpdateChannel and isUpdateAllowedByDesktopConfig validate against a desktopConfig object that specifies allowed versions and channel restrictions. These checks run in the renderer, blocking disallowed updates before network requests initiate.
How does the updater handle macOS-specific installation issues?
Before autoUpdater.quitAndInstall(), the updater.mjs module calls enableSquirrelDirectContentsWrite() to prevent bundle-move failures and cleanStaleUpdaterState() to remove stale ShipIt lock files. These workarounds address common Squirrel.Mac failure modes that can leave the app in a broken state.
Can the update process run silently without user interaction?
Yes. The useElectronUpdaterState hook accepts updateAutoCheck: true and updateAutoDownload: true options. When enabled, updates check and download automatically in the background, with the UI only surfacing the final ready state when installation requires a restart.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →