# OpenWork Electron Desktop App Architecture: IPC, Main Process, and Keychain Security

> Explore the OpenWork Electron app architecture. Learn about the IPC bridge, main process, and keychain security with safeStorage encryption. Get insights into its development.

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

---

**The OpenWork Electron desktop app separates privileged main process operations from the React renderer via a type-safe IPC bridge, using lazy-loaded safeStorage for keychain encryption and a mock-keychain flag for development.**

The `different-ai/openwork` repository implements a sophisticated Electron desktop client that orchestrates a local runtime engine through a strictly controlled inter-process communication layer. Understanding the **OpenWork Electron desktop app architecture** reveals how the main process manages OS-level resources—including the macOS keychain—while exposing a minimal, typed API surface to the React frontend.

## Main Process Architecture and Runtime Bootstrap

The main process serves as the privileged back-end, handling filesystem access, network requests, and native OS integrations.

### Entry Point and Module Registration

In `dev/apps/desktop/electron/main.mjs`, the application initializes the Electron `app` and `BrowserWindow` instances. This module registers all IPC handlers by importing specialized modules like `updater.mjs` and `migration.mjs`, ensuring that privileged operations remain encapsulated in the main thread.

### Runtime Manager and Local Engine

The `dev/apps/desktop/electron/runtime.mjs` module manages the OpenWork "engine"—a local server instance that powers the application's backend services. The **runtime manager** handles workspace discovery, process spawning, and lifecycle events, isolating system-level complexity from the renderer.

### Workspace State Persistence

User-specific configuration and selected workspaces are persisted via `dev/apps/desktop/electron/workspace-store.mjs`. This module operates exclusively in the main process, reading from and writing to the filesystem without exposing direct file access to the frontend.

## Inter-Process Communication (IPC) Flow

The architecture enforces a strict boundary where the React renderer cannot directly access Node.js APIs. Instead, communication flows through Electron's `ipcMain` and `ipcRenderer` via a **preload context bridge**.

### IPC Registration in the Main Process

Privileged handlers are registered in specialized modules. For example, `dev/apps/desktop/electron/updater.mjs` exports `registerUpdaterIpc`, which attaches handlers for `updater:check` and `updater:apply` channels to the `ipcMain` object.

```javascript
// dev/apps/desktop/electron/updater.mjs
export function registerUpdaterIpc({ ipcMain }) {
  ipcMain.handle('updater:check', async () => {
    const info = await resolveArchitectureInfo();
    return info;
  });
  
  ipcMain.handle('updater:apply', async (_, downloadUrl) => {
    return await applyUpdate(downloadUrl);
  });
}

```

Similarly, `dev/apps/desktop/electron/migration.mjs` registers data migration routines via `registerMigrationIpc`.

### Preload Bridge and Renderer Access

The preload script at [`dev/apps/app/src/app/lib/electron-alpha.ts`](https://github.com/different-ai/openwork/blob/main/dev/apps/app/src/app/lib/electron-alpha.ts) exposes a typed `electronAPI` to the renderer using `contextBridge.exposeInMainWorld`. This creates a secure tunnel where React components can invoke main process methods without accessing raw Electron modules.

```typescript
// dev/apps/app/src/app/lib/electron-alpha.ts
contextBridge.exposeInMainWorld('electronAPI', {
  checkForUpdates: () => ipcRenderer.invoke('updater:check'),
  applyUpdate: (url: string) => ipcRenderer.invoke('updater:apply', url),
  onUpdateAvailable: (callback) => ipcRenderer.on('renderer:update-available', callback)
});

```

The renderer consumes this bridge through state modules like [`dev/apps/app/src/react-app/domains/settings/state/electron-updater-state.ts`](https://github.com/different-ai/openwork/blob/main/dev/apps/app/src/react-app/domains/settings/state/electron-updater-state.ts), which manages UI state for the update flow.

## Keychain and Secure Storage Implementation

OpenWork handles sensitive credentials using Electron's `safeStorage` API, implemented with a lazy-loading pattern to defer keychain access until absolutely necessary.

### Lazy-Loaded safeStorage Strategy

Rather than importing `safeStorage` at the top level, the application uses a CommonJS `require` call inside functions that need encryption. This prevents macOS from displaying the keychain authorization dialog during application startup, allowing OpenWork to control when the prompt appears—typically after user authentication.

```javascript
// Encryption utility using lazy-loaded safeStorage
function encryptSecret(secret) {
  const { safeStorage } = require('electron');
  return safeStorage.encryptString(secret);
}

function decryptSecret(encrypted) {
  const { safeStorage } = require('electron');
  return safeStorage.decryptString(encrypted);
}

```

### Mock Keychain for Development

To facilitate development without constant OS prompts, the main process checks for the environment variable `OPENWORK_ELECTRON_USE_MOCK_KEYCHAIN`. When set to `1`, the code executes `app.commandLine.appendSwitch('use-mock-keychain')`, forcing Electron to use a mock keychain that stores credentials in plain text (suitable only for development).

```javascript
// dev/apps/desktop/electron/main.mjs
if (process.env.OPENWORK_ELECTRON_USE_MOCK_KEYCHAIN === '1') {
  app.commandLine.appendSwitch('use-mock-keychain');
}

```

## UI Control Server and Desktop Automation

Beyond standard IPC, the architecture includes an HTTP control server and automation capabilities. The `dev/apps/desktop/electron/ui-control-server.mjs` module exposes a local HTTP endpoint allowing external tools to trigger UI actions, while `dev/apps/desktop/electron/automation-runner.mjs` provides `createDesktopAutomationRunner` for automated testing via the Chrome DevTools Protocol.

## Summary

- The **main process** in `main.mjs` bootstraps the Electron app, manages the local runtime engine via `runtime.mjs`, and registers all IPC handlers.
- **IPC communication** is strictly typed and funneled through a preload bridge ([`electron-alpha.ts`](https://github.com/different-ai/openwork/blob/main/electron-alpha.ts)), preventing direct Node.js access from the React renderer.
- **Keychain security** relies on lazy-loaded `safeStorage` to defer OS prompts, with a mock-keychain option controlled by the `OPENWORK_ELECTRON_USE_MOCK_KEYCHAIN` environment variable.
- **Workspace state** persists in the main process via `workspace-store.mjs`, while `ui-control-server.mjs` enables external HTTP control for automation workflows.

## Frequently Asked Questions

### How does OpenWork handle keychain access without prompting users during development?

The application checks for the `OPENWORK_ELECTRON_USE_MOCK_KEYCHAIN` environment variable in `main.mjs`. When enabled, it appends the `use-mock-keychain` command-line switch, which instructs Electron to use a mock keychain instead of the system keychain, eliminating authorization dialogs during development.

### What is the role of the preload script in OpenWork's IPC architecture?

The preload script ([`electron-alpha.ts`](https://github.com/different-ai/openwork/blob/main/electron-alpha.ts)) uses `contextBridge.exposeInMainWorld` to inject a typed `electronAPI` object into the renderer's global scope. This exposes only specific IPC channels—such as `updater:check`—while blocking access to sensitive Electron modules, maintaining the security boundary between main and renderer processes.

### How does the main process communicate update availability to the React frontend?

After downloading an update, the main process uses `mainWindow.webContents.send('renderer:update-available', payload)` to push a message from `updater.mjs` to the renderer. The React frontend listens for this event via the exposed `electronAPI` and updates its state accordingly, as managed in [`electron-updater-state.ts`](https://github.com/different-ai/openwork/blob/main/electron-updater-state.ts).

### Where is workspace configuration stored in the OpenWork desktop app?

Workspace configuration is managed exclusively by the main process in `dev/apps/desktop/electron/workspace-store.mjs`. This module handles reading and writing workspace metadata to the filesystem, ensuring that the renderer process cannot directly access the file system and maintaining data integrity.