OpenWork Headless Web Mode Without Electron: Architecture and Implementation

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. 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 (lines 18-23), the script checks for an existing manifest at 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 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 (lines 78-115) merges the isolated server configuration (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 with merged configuration and CORS origins (lines 234-246). Finally, dev-headless-web-lib.ts (lines 162-208) builds a runtime manifest at 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 (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): Orchestrates the entire stack, implements instance reuse logic, and manages process lifecycles.

  • Helper Library (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.

  • Runtime Manifest (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. 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:

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:

pnpm dev:headless-web

If 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:

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:

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:

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.

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.

  • Process management relies on detached child processes, runtime manifests at tmp/dev-headless-web.json, and graceful signal handling to maintain state across sessions.

  • Configuration persistence occurs through 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 utility generates a token for owner access and a hostToken for administrative functions, stored in 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 (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, 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 (lines 78-115). The server maintains an isolated configuration at 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →