# Electron Auto-Updater in OpenWork: How It Works and How to Customize It

> Discover how OpenWork leverages electron-updater for seamless auto-updates. Learn about GitHub release channeling, stable/alpha channels, and the signed recovery installer.

- Repository: [Different AI/openwork](https://github.com/different-ai/openwork)
- Tags: how-to-guide
- Published: 2026-08-16

---

**OpenWork uses electron-updater with a custom main-process module that channels releases from GitHub, supports stable/alpha channels, and includes a signed recovery installer fallback for corrupted updates.**

OpenWork ships its desktop client as an Electron application with a production-ready auto-update system. The Electron updater mechanism handles version detection, release channel selection, and fallback recovery—all exposed through a clean IPC bridge to the React frontend.

## What Is the Electron Updater in OpenWork?

The **electron-updater** module (`autoUpdater` API) powers the auto-update mechanism in OpenWork. Unlike the basic Electron `autoUpdater`, this implementation adds **channel-based releases** (stable vs. alpha), **macOS Squirrel bundle fixes**, and a **recovery flow** for failed updates.

Key capabilities include:

- **Channel-aware updates** – Users opt into `stable` (default) or `alpha` (macOS-only) releases
- **Full-zip downloads** – Disabled differential downloads for reliability
- **Progress tracking** – Real-time download percentage in the UI
- **Signed recovery installers** – Fallback when normal updates fail

## Core Updater Architecture

The auto-update system spans the main process, IPC layer, and renderer. Here's how the pieces connect:

| File | Purpose |
|------|---------|
| `apps/desktop/electron/updater.mjs` | Main-process implementation: version detection, feed URL building, autoUpdater configuration |
| `apps/desktop/electron/recovery.mjs` | Recovery installer download, verification, and execution |
| [`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) | React hook (`useElectronUpdaterState`) managing UI state and IPC calls |
| [`apps/app/src/react-app/domains/settings/pages/updates-view.tsx`](https://github.com/different-ai/openwork/blob/main/apps/app/src/react-app/domains/settings/pages/updates-view.tsx) | Settings page rendering update controls |
| [`apps/app/src/app/lib/platform-capabilities.ts`](https://github.com/different-ai/openwork/blob/main/apps/app/src/app/lib/platform-capabilities.ts) | Declares `autoUpdate` capability (hides UI on non-Electron platforms) |

### Version Detection and Channel Selection

In `updater.mjs`, the `resolveAppVersion` function detects the running version:

- **Packaged builds**: Uses `app.getVersion()`
- **Development**: Reads from [`package.json`](https://github.com/different-ai/openwork/blob/main/package.json)

The release channel persists in [`electron-updater-channel.v1.json`](https://github.com/different-ai/openwork/blob/main/electron-updater-channel.v1.json) within `app.getPath('userData')`. The `readElectronUpdaterChannel` and `writeElectronUpdaterChannel` functions manage this state.

### Feed URL Configuration

The `ELECTRON_UPDATER_FEEDS` object maps channels to GitHub release endpoints:

| Channel | Feed URL |
|---------|----------|
| `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 applies the correct feed to the autoUpdater instance.

## How to Check for Updates Programmatically

### From the React Frontend

Use the `useElectronUpdaterState` hook to trigger update checks:

```typescript
// In a React component
const {
  checkForUpdates,
  updateStatus,
  downloadUpdate,
  installUpdateAndRestart,
} = useElectronUpdaterState();

// Manual "Check for updates" button handler
const onCheck = async () => {
  const result = await checkForUpdates(); // Returns status with version info if available
  console.log(result.updateInfo?.version);
};

```

The hook calls `window.electronAPI.invoke('openwork:updater:check')`, which routes to the main-process handler.

### Main-Process Handler

In `updater.mjs`, the IPC handler implements the check:

```typescript
ipcMain.handle("openwork:updater:check", async (_event, rawChannel, rawTargetVersion) => {
  const updater = await ensureAutoUpdater();          // Loads electron-updater with config
  const channelState = await applyElectronUpdaterFeed(app, updater, targetVersion, manifestChannel);
  const result = await updater.checkForUpdates();     // HTTP request to feed URL
  return buildUpdateCheckResponse(result, channelState);
});

```

## How to Download and Install Updates

### Download Progress Tracking

The download step streams the full update zip with progress events:

```typescript
// Frontend: initiate download
const onDownload = async () => {
  const result = await downloadUpdate();
  if (!result.ok) {
    console.error("Download failed:", result.reason);
  }
};

```

Main-process implementation in `updater.mjs`:

```typescript
ipcMain.handle("openwork:updater:download", async () => {
  const updater = await ensureAutoUpdater();
  await updater.downloadUpdate();   // Emits "download-progress" events
  return { ok: true };
});

```

Progress events forward to the renderer via `autoUpdater.on('download-progress', ...)`, updating `updateStatus.downloadedBytes` and `totalBytes` in the hook.

### Install and Restart

The `quitAndInstall` call swaps the bundle and relaunches:

```typescript
// Frontend: install and restart
const onInstall = async () => {
  await installUpdateAndRestart(); // App quits immediately on success
};

```

Main-process handler with macOS safety fix:

```typescript
ipcMain.handle("openwork:updater:installAndRestart", async () => {
  const updater = await ensureAutoUpdater();
  await enableSquirrelDirectContentsWrite(); // Prevents "Failed to copy bundle" errors
  updater.quitAndInstall(false, true);       // isSilent=false, runAfter=true
  return { ok: true };
});

```

## How to Switch Release Channels

The `setReleaseChannel` function opts users into alpha releases:

```typescript
await setReleaseChannel("alpha"); // UI call

```

Behind the scenes in `updater.mjs`:

```typescript
ipcMain.handle("openwork:updater:setChannel", async (_event, rawChannel) => {
  const channel = await writeElectronUpdaterChannel(app, rawChannel, manifestChannel);
  const updater = await ensureAutoUpdater();
  if (updater) {
    preventPendingUpdaterInstall(updater);        // Cancel any pending update
    return applyElectronUpdaterFeed(app, updater, null, manifestChannel);
  }
  return updaterChannelState(app, channel, null, manifestChannel);
});

```

The channel change persists across app restarts via the JSON file in user data.

## Recovery Mechanism for Failed Updates

When normal updates fail (corrupted bundles, signature mismatches), the recovery flow in `recovery.mjs` provides a fallback:

1. **Download signed installer** – Fetches `OpenWork-Recovery.dmg` (macOS) from release assets
2. **Verify SHA-512** – Checks against embedded manifest hash
3. **Cache locally** – Stores in `app.getPath('userData')/recovery/`
4. **Launch via shell** – Executes the installer and exits the app

Recovery IPC handlers use the `openwork:recovery:*` namespace, exposed through parallel hooks in the UI.

## Auto-Updater Configuration Details

The `ensureAutoUpdater` function in `updater.mjs` applies these settings:

| Setting | Value | Purpose |
|---------|-------|---------|
| `autoDownload` | `false` | Manual download control for progress UI |
| `disableDifferentialDownload` | `true` | Full-zip downloads only (blockmap skipped) |
| `SquirrelMacDirectContentsWrite` | `true` | Fixes macOS bundle copy failures |

These settings prioritize reliability over download size, appropriate for OpenWork's GitHub-releases distribution model.

## Summary

- **Version detection** uses `app.getVersion()` in production, [`package.json`](https://github.com/different-ai/openwork/blob/main/package.json) in development—implemented in `updater.mjs`
- **Channel persistence** stores user preference in [`electron-updater-channel.v1.json`](https://github.com/different-ai/openwork/blob/main/electron-updater-channel.v1.json) under user data
- **Feed resolution** maps `stable` and `alpha` channels to distinct GitHub release URLs
- **AutoUpdater configuration** disables differential downloads and enables macOS Squirrel fixes
- **IPC bridge** (`openwork:updater:*`) exposes check, download, and install operations to the renderer
- **React integration** through `useElectronUpdaterState` hook with state machine (`idle` → `checking` → `available` → `downloading` → `ready`)
- **Recovery fallback** downloads signed installers when standard updates fail, implemented in `recovery.mjs`

## Frequently Asked Questions

### How does OpenWork detect if auto-updates should run?

OpenWork checks `app.isPackaged` at startup. When `false` (development), `updater.mjs` exports no-op functions. When `true`, it loads the full electron-updater stack and registers IPC handlers.

### Can users opt out of automatic update checks?

Yes. The `useElectronUpdaterState` hook accepts an `autoCheck` option. When `false`, the app skips the initial `checkForUpdates()` call on mount. Users manually trigger checks via the Settings UI.

### Why does OpenWork disable differential downloads?

Differential downloads (blockmap-based) sometimes fail with GitHub releases due to CDN inconsistencies. Setting `disableDifferentialDownload = true` in `ensureAutoUpdater` ensures full-zip reliability at the cost of larger downloads.

### What happens if the alpha channel is selected on Windows or Linux?

The `alpha` channel feed URL is macOS-specific (`alpha-macos-latest`). The `applyElectronUpdaterFeed` function returns an error state for non-macOS platforms, and the UI blocks channel selection via [`platform-capabilities.ts`](https://github.com/different-ai/openwork/blob/main/platform-capabilities.ts) detection.