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 from app.getVersion()
  • updateEnv – {supported: boolean, reason?: string}
  • updateStatus – State machine: idle | checking | available | downloading | ready | error
  • checkForUpdates() – Triggers manual or scheduled update checks
  • downloadUpdate() – Starts download with progress callbacks
  • installUpdateAndRestart() – Quits and installs the pending update
  • setReleaseChannel(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 failures
  • cleanStaleUpdaterState() – 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 policy
  • isUpdateAllowedByDesktopConfig – Validates against allowedDesktopVersions list
  • isAlphaUpdateAllowed – 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

  1. App startup – registerUpdaterIpc initializes handlers; renderer receives bridge object
  2. Channel resolution – UI loads persisted channel, applies org policy via resolveDesktopUpdateChannel
  3. Auto-check trigger – Scheduled check calls bridge.check(), which runs autoUpdater.checkForUpdates()
  4. Update available – Hook transitions to updateStatus.state = "available" with version details
  5. Download – downloadUpdate() registers progress handler, streams bytes, reaches state = "ready"
  6. Install – installUpdateAndRestart() invokes autoUpdater.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.ts prevents 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →