# OpenWork Electron Updater Mechanism: How Auto-Updates Work in the Desktop App

> Discover how OpenWork's desktop app leverages electron-updater for seamless auto-updates. Learn about signed updates, multiple channels, and policy adherence.

- Repository: [Different AI/openwork](https://github.com/different-ai/openwork)
- Tags: internals
- Published: 2026-08-15

---

**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/main/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`](https://github.com/different-ai/openwork/blob/dev/apps/desktop/electron/updater.mjs#L1-L130) |

## Renderer-Side: The `useElectronUpdaterState` Hook

The React-facing API lives in **[`apps/app/src/react-app/domains/settings/state/electron-updater-state.ts`](https://github.com/different-ai/openwork/blob/main/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:

```typescript
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:

```typescript
// 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:

```javascript
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**:

```javascript
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:

```javascript
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**:

```javascript
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:

```javascript
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`](https://github.com/different-ai/openwork/blob/main/version-gate.ts)** module (at [`apps/app/src/app/lib/version-gate.ts`](https://github.com/different-ai/openwork/blob/main/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:

```yaml

# 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`](https://github.com/different-ai/openwork/blob/main/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

```tsx
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)

```typescript
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

```typescript
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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/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.