# OpenWork Headless Web Mode Without Electron: Architecture and Implementation

> Explore OpenWork's headless web mode architecture. Build desktop like experiences with a Vite server and OpenWork server, eliminating the need for Electron. Learn more today.

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

---

**OpenWork's headless-web mode provides a fully functional desktop-like experience using only a Vite web server and the OpenWork server process—no Electron binary is involved.**

The `different-ai/openwork` repository implements a lightweight, Electron-free architecture for running the OpenWork desktop application as a headless web service. By orchestrating a Vite development server and the OpenWork server process through a custom launcher, this mode eliminates the need for Chromium-based desktop shells while preserving full workspace functionality and API access.

## Startup Sequence and Orchestration

The headless mode follows a strict initialization protocol defined in [`scripts/dev-headless-web.ts`](https://github.com/different-ai/openwork/blob/main/scripts/dev-headless-web.ts). This orchestration ensures process isolation, configuration persistence, and graceful lifecycle management.

### Environment Preparation and Instance Detection

Before spawning processes, the launcher creates a `tmp/` directory to store runtime logs and the manifest file. In [`scripts/dev-headless-web.ts`](https://github.com/different-ai/openwork/blob/main/scripts/dev-headless-web.ts) (lines 18-23), the script checks for an existing manifest at [`tmp/dev-headless-web.json`](https://github.com/different-ai/openwork/blob/main/tmp/dev-headless-web.json). If the health endpoints respond and the `--replace` flag is absent, the script reuses the running instance rather than spawning new processes (lines 71-86).

### Port Resolution and Configuration Merging

The system dynamically allocates network ports to avoid collisions. Lines 107-137 in [`dev-headless-web.ts`](https://github.com/different-ai/openwork/blob/main/dev-headless-web.ts) select free ports for the Vite UI (default 5178) and the OpenWork server (default 8778). Subsequently, the helper library [`scripts/dev-headless-web-lib.ts`](https://github.com/different-ai/openwork/blob/main/scripts/dev-headless-web-lib.ts) (lines 78-115) merges the isolated server configuration ([`tmp/headless-server.json`](https://github.com/different-ai/openwork/blob/main/tmp/headless-server.json)) with the current workspace settings, ensuring that UI-created workspaces survive server restarts.

### Process Spawning and Manifest Creation

The launcher spawns two detached child processes. First, it executes `pnpm --filter @openwork/app exec vite` with custom `VITE_*` environment variables to start the React UI (lines 210-226). Second, it launches the OpenWork server via Bun, executing [`apps/server/src/cli.ts`](https://github.com/different-ai/openwork/blob/main/apps/server/src/cli.ts) with merged configuration and CORS origins (lines 234-246). Finally, [`dev-headless-web-lib.ts`](https://github.com/different-ai/openwork/blob/main/dev-headless-web-lib.ts) (lines 162-208) builds a runtime manifest at [`tmp/dev-headless-web.json`](https://github.com/different-ai/openwork/blob/main/tmp/dev-headless-web.json) containing URLs, authentication tokens, PIDs, workspace paths, and optional Den proxy settings.

### Lifecycle and Signal Handling

The launcher implements robust process management in [`dev-headless-web.ts`](https://github.com/different-ai/openwork/blob/main/dev-headless-web.ts) (lines 257-284). It catches `SIGINT` and `SIGTERM` signals to terminate all child processes gracefully while preserving the manifest for subsequent reuse. Each process runs in its own process group, preventing terminal session termination from cascading to the server stack.

## Key Architectural Components

The headless architecture consists of six primary components working in concert:

- **Launcher Script** ([`scripts/dev-headless-web.ts`](https://github.com/different-ai/openwork/blob/main/scripts/dev-headless-web.ts)): Orchestrates the entire stack, implements instance reuse logic, and manages process lifecycles.

- **Helper Library** ([`scripts/dev-headless-web-lib.ts`](https://github.com/different-ai/openwork/blob/main/scripts/dev-headless-web-lib.ts)): Provides utilities for cryptographically secure token generation, configuration merging, CORS origin building, and manifest serialization.

- **Vite Development Server**: Serves the React UI (`@openwork/app`) on the dynamically selected web port (default 5178), providing hot module replacement and asset serving without Electron's renderer process.

- **OpenWork Server**: The local API layer running on port 8778 (by default), handling workspaces, sessions, and plugin execution. Entry point is [`apps/server/src/cli.ts`](https://github.com/different-ai/openwork/blob/main/apps/server/src/cli.ts).

- **Runtime Manifest** ([`tmp/dev-headless-web.json`](https://github.com/different-ai/openwork/blob/main/tmp/dev-headless-web.json)): JSON configuration file exposing the web URL, API endpoint, authentication tokens, process IDs, and Den proxy configuration for downstream tooling and test runners.

- **Temporary Log Files**: Isolated output streams for the Vite process (`tmp/dev-web.log`) and the OpenWork server (`tmp/dev-headless.log`) facilitating debugging without console clutter.

## How the Architecture Eliminates Electron Dependencies

OpenWork achieves desktop-like functionality without Electron through four specific architectural decisions:

**Browser-Based UI Delivery**: The interface is a standard React web bundle served by Vite rather than a Chromium Embedded Framework window. Users access the application by opening a browser tab at the Vite URL, eliminating the Electron binary and its associated memory overhead.

**Process Isolation**: The launcher spawns the UI and server as *detached* child processes with independent process groups. This architecture prevents terminal session termination from killing the application stack, a role typically handled by Electron's main process.

**Same-Origin Proxying**: When integrating with Den (the cloud control plane), the Vite development server proxies `/api/den` requests to the remote Den target. This maintains same-origin policy compliance for all API calls, removing the need for Electron's IPC bridge for cross-origin requests.

**Token-Based Authentication**: The manifest stores a **token** (owner access) and **hostToken** (administrative access) generated in [`dev-headless-web-lib.ts`](https://github.com/different-ai/openwork/blob/main/dev-headless-web-lib.ts). These tokens exchange via HTTP headers between the UI and server, replacing Electron's secure storage mechanisms with standard web authentication patterns.

## Practical Usage Examples

### Starting a Detached Headless Stack

Run the following command to start the headless web mode in the background:

```bash
pnpm dev:headless-web --detach

```

The launcher outputs the accessible endpoints:

```

[dev:headless-web] Web URL: http://127.0.0.1:5178
[dev:headless-web] OpenWork server: http://127.0.0.1:8778

```

### Reusing Existing Instances

To connect to a running instance without spawning new processes:

```bash
pnpm dev:headless-web

```

If [`tmp/dev-headless-web.json`](https://github.com/different-ai/openwork/blob/main/tmp/dev-headless-web.json) references a healthy stack, the script prints the existing URLs and exits immediately.

### Forcing a Fresh Start

Terminate existing processes and initialize new ones using the replace flag:

```bash
pnpm dev:headless-web --replace

```

The launcher reads the old manifest, sends `SIGTERM` followed by `SIGKILL` to the recorded PIDs, then spawns fresh Vite and server processes.

### Programmatic Manifest Access

Access the runtime configuration programmatically to integrate with test runners or external tooling:

```typescript
import { readFile } from "node:fs/promises";

async function loadManifest() {
  const data = await readFile("tmp/dev-headless-web.json", "utf8");
  const manifest = JSON.parse(data);
  console.log("Web UI:", manifest.webUrl);
  console.log("API endpoint:", manifest.openworkUrl);
  console.log("Auth token:", manifest.token);
}

loadManifest();

```

### Custom Workspace and Proxy Configuration

Launch with specific workspace directories or disable Den proxying:

```bash
OPENWORK_WORKSPACE=/path/to/my/workspace \
OPENWORK_DEV_DEN_PROXY=0 \
pnpm dev:headless-web

```

Setting `OPENWORK_DEV_DEN_PROXY=0` disables the Den proxy, while `OPENWORK_WORKSPACE` specifies the persistent workspace directory passed to [`apps/server/src/cli.ts`](https://github.com/different-ai/openwork/blob/main/apps/server/src/cli.ts).

## Summary

- **OpenWork's headless-web mode** eliminates Electron by using a Vite server for the UI and a Bun-powered server for the API, coordinated through [`scripts/dev-headless-web.ts`](https://github.com/different-ai/openwork/blob/main/scripts/dev-headless-web.ts).

- **Process management** relies on detached child processes, runtime manifests at [`tmp/dev-headless-web.json`](https://github.com/different-ai/openwork/blob/main/tmp/dev-headless-web.json), and graceful signal handling to maintain state across sessions.

- **Configuration persistence** occurs through [`tmp/headless-server.json`](https://github.com/different-ai/openwork/blob/main/tmp/headless-server.json), ensuring workspaces survive restarts without user intervention.

- **Authentication** uses cryptographically secure tokens stored in the manifest and transmitted via HTTP headers, replacing Electron's secure storage.

- **Development workflow** supports hot instance reuse, forced replacement via `--replace`, and custom environment variables for workspace and proxy configuration.

## Frequently Asked Questions

### How does OpenWork authenticate users without Electron's secure storage?

According to the `different-ai/openwork` source code, authentication relies on token-based security. The [`dev-headless-web-lib.ts`](https://github.com/different-ai/openwork/blob/main/dev-headless-web-lib.ts) utility generates a **token** for owner access and a **hostToken** for administrative functions, stored in [`tmp/dev-headless-web.json`](https://github.com/different-ai/openwork/blob/main/tmp/dev-headless-web.json). These tokens transmit via standard HTTP headers between the Vite-served UI and the OpenWork server, eliminating the need for Electron-specific secure storage APIs while maintaining security through cryptographically random values.

### Can multiple headless instances run simultaneously on the same machine?

Yes, but with port coordination. The launcher in [`scripts/dev-headless-web.ts`](https://github.com/different-ai/openwork/blob/main/scripts/dev-headless-web.ts) (lines 107-137) automatically detects and selects free ports if the defaults (5178 for Vite, 8778 for the server) are occupied. Each instance writes its own manifest to [`tmp/dev-headless-web.json`](https://github.com/different-ai/openwork/blob/main/tmp/dev-headless-web.json), though only one manifest is preserved at a time. To run multiple isolated instances permanently, specify distinct `OPENWORK_WORKSPACE` values and temporary directories.

### What happens to workspace data when the headless server restarts?

Workspace data persists through configuration merging implemented in [`scripts/dev-headless-web-lib.ts`](https://github.com/different-ai/openwork/blob/main/scripts/dev-headless-web-lib.ts) (lines 78-115). The server maintains an isolated configuration at [`tmp/headless-server.json`](https://github.com/different-ai/openwork/blob/main/tmp/headless-server.json) that records registered workspaces. When the launcher restarts, it merges this persisted configuration with the current environment, ensuring that projects created through the UI remain accessible after the OpenWork server process restarts.

### How does the Vite server communicate with the OpenWork API without Electron's IPC?

The architecture uses standard HTTP requests and a same-origin proxy pattern. The Vite development server serves the UI on one port while the OpenWork API runs on another. For cloud control plane (Den) integration, Vite proxies `/api/den` requests to the remote target, maintaining same-origin policy compliance. For local API calls, the UI connects directly to `http://127.0.0.1:8778` using the tokens from the manifest, bypassing the need for Electron's IPC bridge entirely.