# OpenWork Desktop Electron App Architecture: Main Process, Renderer, and Runtime Manager Explained

> Explore the OpenWork desktop Electron app architecture. Understand the main process, renderer, and runtime manager for seamless native OS integration and a React UI.

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

---

**The OpenWork desktop application implements a two-process Electron architecture where the main Node.js process manages native OS integrations and spawns the OpenWork server as a child process, while the Chromium renderer hosts a React UI that communicates via secure IPC channels.**

The OpenWork desktop client from the `different-ai/openwork` repository is a cross-platform Electron application that orchestrates a React-based user interface with an embedded Go backend. Understanding the architecture of the OpenWork desktop Electron app reveals how it handles workspace management, protocol deep-linking, and external automation through a strict separation of concerns between the main process, renderer process, and runtime manager.

## Two-Process Electron Architecture Overview

OpenWork follows Electron’s standard multi-process model with distinct responsibilities:

- **Main Process** (`apps/desktop/electron/main.mjs`): Runs Node.js with full OS access, creates the `BrowserWindow`, registers the `openwork://` protocol, and manages child processes.
- **Renderer Process**: A Vite-built React application (`@openwork/app`) running in Chromium that renders the UI and communicates with the main process via `ipcRenderer`.

This separation ensures that UI code remains sandboxed while system-level operations—such as spawning the OpenWork server or handling auto-updates—execute in the privileged main process.

## Main Process Native Integration (`main.mjs`)

The entry point at `apps/desktop/electron/main.mjs` bootstraps the entire application lifecycle.

### Bootstrap and Protocol Registration

On startup, the main process sets the app identifier (`APP_IDENTIFIER`), configures the user-data path, and registers the custom `openwork://` protocol for deep-linking. It initializes the main window via `createMainWindow()` and loads the renderer bundle:

```ts
import { app, BrowserWindow } from "electron";
import { createRuntimeManager } from "./runtime.mjs";
import { createWorkspaceStore } from "./workspace-store.mjs";
import { createUiControlServer } from "./ui-control-server.mjs";

app.whenReady().then(async () => {
  const workspaceStore = createWorkspaceStore({ app });
  const runtimeManager = createRuntimeManager({ app, workspaceStore });
  const uiServer = createUiControlServer({ app });
  
  const win = new BrowserWindow({
    width: 1280,
    height: 800,
    webPreferences: { preload: path.join(__dirname, "preload.mjs") },
  });
  await win.loadURL(`file://${path.join(__dirname, "../../app/dist/index.html")}`);
});

```

### Core Service Initialization

The main process imports and instantiates four critical utilities at the top of `main.mjs`:

- **`createRuntimeManager`** (`./runtime.mjs`): Manages the lifecycle of the OpenWork server child process.
- **`createWorkspaceStore`** (`./workspace-store.mjs`): Persists workspace metadata and configuration.
- **`createUiControlServer`** (`./ui-control-server.mjs`): Exposes a local HTTP/WebSocket endpoint for external automation.
- **`createApplicationMenu`** (`./app-menu.mjs`): Builds native application menus.

Additionally, `registerUpdaterIpc` (`./updater.mjs`) wires the auto-update logic for Windows and macOS, while deep-link handlers listen for `openwork:deep-link-native` events to route URLs between the OS and the renderer.

## Runtime Manager and Sidecar Binaries (`runtime.mjs`)

Located at `apps/desktop/electron/runtime.mjs`, the **Runtime Manager** isolates the OpenWork server—a Go-based HTTP service—from the UI layer.

### Port Allocation and Process Spawning

When a user opens a workspace, `runtimeManager.engineStart()` resolves a free TCP port in the range 48000-51000 using `findFreePort`, then spawns the server binary:

```ts
export async function engineStart(workspacePath, opts = {}) {
  const { port } = await resolveOpenworkPort("127.0.0.1", workspaceKey);
  const env = await buildChildEnv({ OPENWORK_SERVER_PORT: `${port}` });
  const child = spawn(opencodeBinaryPath, [
    "serve", 
    "--workspace", workspacePath, 
    "--port", `${port}`
  ], { env });

  engineState.child = child;
  engineState.port = port;
  engineState.baseUrl = `http://127.0.0.1:${port}`;
}

```

The manager tracks **engine states** (`child`, `port`, `baseUrl`) and exposes `engineStop`, `engineRestart`, and `dispose` routines for graceful shutdown when the app quits.

### System CA and Docker Integration

The Runtime Manager handles enterprise TLS environments by exporting host root certificates to the child process via `NODE_EXTRA_CA_CERTS` using `system-ca.mjs`. It also detects Docker binaries and can launch server containers when configured, bridging the gap between the Electron host and containerized sidecars stored in `apps/desktop/resources/sidecars/`.

## Workspace Persistence and Connect-Link Flow

### Workspace Store (`workspace-store.mjs`)

The `createWorkspaceStore` utility persists data under Electron’s user-data directory, maintaining:

- Local workspace paths and remote Den URLs.
- Desktop bootstrap configuration (branding, activation state).
- The active/selected workspace ID.

It provides `listLocalWorkspacePaths()` to the Runtime Manager and APIs for Connect-Link acceptance (`acceptConnectLink`) and branding persistence (`persistConnectLinkClaims`).

### Deep-Link Handling (`connect-link.mjs`)

The Connect-Link flow at `apps/desktop/electron/connect-link.mjs` handles `openwork://connect?...` URLs used by remote MCPs (Model Context Protocols). The main process verifies signatures against bundled public keys, resolves the URL into a Den-hosted connection, and caches brand icons to the user-data folder after validating size and aspect ratio. This ensures the UI immediately reflects new branding without reloading the renderer.

## UI-Control Server for External Automation

The `createUiControlServer` function in `apps/desktop/electron/ui-control-server.mjs` starts a local HTTP and WebSocket server (default port 9223, overrideable via `OPENWORK_ELECTRON_REMOTE_DEBUG_PORT`). This server allows external agents—such as the `opencode-chrome-devtools` plugin—to drive the desktop UI through commands like creating workspaces or triggering model picks. It also forwards Chrome DevTools Protocol (CDP) commands to the embedded browser panel (`browser-panel.mjs`), enabling programmatic UI automation.

## Renderer Process React Application

### Vite Build and Entry Point

The renderer is a conventional React codebase built with Vite. The entry point is [`apps/app/src/app/app.tsx`](https://github.com/different-ai/openwork/blob/main/apps/app/src/app/app.tsx), which renders the full OpenWork UI including workspace lists, chat interfaces, and plugin panels. State management uses **Zustand**, while OpenWork skills load via the `@openwork/ui` and `@openwork/types` packages.

### Secure IPC via Preload Script

Communication with the main process flows through `apps/desktop/electron/preload.mjs`, which exposes a safe API via `contextBridge`:

```js
const { contextBridge, ipcRenderer } = require("electron");

contextBridge.exposeInMainWorld("electron", {
  sendDeepLink: (url) => ipcRenderer.send("openwork:deep-link-native", [url]),
  onDeepLink: (cb) => ipcRenderer.on("openwork:deep-link-native", (_, urls) => cb(urls)),
  onNotification: (cb) => ipcRenderer.on("desktop-notification", (_, data) => cb(data)),
});

```

The renderer imports these utilities from [`apps/app/src/app/lib/electron-alpha.ts`](https://github.com/different-ai/openwork/blob/main/apps/app/src/app/lib/electron-alpha.ts), ensuring that OS notifications, deep-link handling, and branding updates remain sandboxed yet functional.

## Build Configuration and Distribution

Packaging is handled by **Electron Builder** using configuration files ([`electron-builder.yml`](https://github.com/different-ai/openwork/blob/main/electron-builder.yml), [`electron-builder.base.yml`](https://github.com/different-ai/openwork/blob/main/electron-builder.base.yml)) that define platform-specific signing, icons, and sidecar inclusion. Sidecar binaries reside in `apps/desktop/resources/sidecars/` and are referenced by the Runtime Manager during the `engineStart` sequence. The build pipeline is driven by pnpm scripts defined in [`apps/app/package.json`](https://github.com/different-ai/openwork/blob/main/apps/app/package.json), with workspace bootstrap constants stored in [`constants.json`](https://github.com/different-ai/openwork/blob/main/constants.json) (including the OpenCode version for sidecar installation).

## Summary

- **Main Process** (`main.mjs`): Bootstraps Electron, registers the `openwork://` protocol, manages the application lifecycle, and hosts core services including the Runtime Manager, Workspace Store, and UI-Control Server.
- **Runtime Manager** (`runtime.mjs`): Spawns the OpenWork Go server as a child process, handles port allocation (48000-51000), system CA certificates, and Docker integration.
- **Workspace Store** (`workspace-store.mjs`): Persists workspace metadata and Connect-Link claims to the user-data directory.
- **UI-Control Server** (`ui-control-server.mjs`): Exposes port 9223 for external HTTP/WebSocket automation and CDP forwarding.
- **Renderer Process**: A Vite-built React app using Zustand state management, communicating with the main process via a secure `preload.mjs` bridge.
- **Build Pipeline**: Electron Builder packages the app with sidecar binaries from `resources/sidecars/`, managed by pnpm scripts in the `apps/app` workspace.

## Frequently Asked Questions

### How does the OpenWork Electron app handle deep-linking from external browsers?

The main process registers the `openwork://` protocol during bootstrap. When the OS receives a deep-link, Electron emits an `openwork:deep-link-native` event in `main.mjs`, which parses the URL via `connect-link.mjs` and forwards it to the renderer through the `preload.mjs` bridge. The renderer then handles workspace selection or Connect-Link verification accordingly.

### What is the purpose of the UI-Control Server on port 9223?

The UI-Control Server provides a local HTTP and WebSocket endpoint that enables external automation tools—such as browser extensions or AI agents—to programmatically control the OpenWork UI. It accepts commands to create workspaces, open files, and forward Chrome DevTools Protocol messages to the embedded browser panel, facilitating integration with tools like `opencode-chrome-devtools`.

### How does the Runtime Manager ensure the OpenWork server starts reliably?

The Runtime Manager in `runtime.mjs` locates an available TCP port in the 48000-51000 range using `findFreePort`, constructs the appropriate environment variables (including `NODE_EXTRA_CA_CERTS` for enterprise TLS), and spawns the OpenCode binary as a child process. It maintains engine state to support graceful shutdowns, restarts, and port reassignment without requiring the user to manually configure network settings.

### Where are workspace configurations and branding assets stored?

Workspace metadata, selected workspace IDs, and Connect-Link claims are persisted via the Workspace Store to Electron’s user-data directory. Branding assets—such as custom icons downloaded through Connect-Links—are validated for size and aspect ratio, then cached to the same user-data folder and applied to the taskbar, dock, or window frame by the main process.