# How OpenWork Embedded Server Mode Works for Local Development

> Discover how OpenWork's embedded server mode simplifies local development. Run a stand-alone HTTP server in-process with startEmbeddedServer() for efficient testing without separate Node processes.

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

---

**OpenWork's embedded server mode runs a stand-alone HTTP server in-process via `startEmbeddedServer()`, enabling local development and testing without spawning separate Node processes while optionally managing a child OpenCode engine.**

The `different-ai/openwork` repository provides an **embedded server mode** that allows developers to launch the OpenWork backend directly within their current process. This mode eliminates the need to manage separate Node processes during local development or automated testing. By calling `startEmbeddedServer()` from [`apps/server/src/embedded.ts`](https://github.com/different-ai/openwork/blob/main/apps/server/src/embedded.ts), you can spin up a fully functional HTTP server with optional OpenCode engine management in milliseconds.

## What Is OpenWork Embedded Server Mode?

OpenWork ships with a **stand-alone HTTP server** capable of running **in-process** as an embedded server. This architecture powers the CLI (`openwork-server`) and enables automated tests to start the OpenWork backend programmatically. Unlike traditional server deployments that require process isolation, the embedded mode runs entirely inside the current Node process, making it ideal for rapid iteration and isolated test environments.

## The Embedded Server Startup Sequence

When `startEmbeddedServer()` is invoked, it executes a precise initialization pipeline defined in [`apps/server/src/embedded.ts`](https://github.com/different-ai/openwork/blob/main/apps/server/src/embedded.ts).

### Configuration Resolution

The process begins with **`resolveServerConfig()`**, which merges CLI arguments, environment variables, and default values into a unified `ServerConfig` object. This configuration dictates port binding, authentication tokens, and workspace definitions before any server components initialize.

### Workspace Initialization

If the server operates in read-write mode, **`ensureLocalWorkspaceFiles()`** creates the necessary workspace directories on disk. This step ensures that all configured workspaces exist and are ready to receive runtime data before the HTTP server accepts connections.

### Managed OpenCode Initialization (Optional)

When the caller sets `manageOpencode: true` and omits `opencodeBaseUrl`, the embedded server spawns a managed OpenCode child process:

1. **`findManagedEngineWorkspace`** identifies a workspace to host the OpenCode engine.
2. **`writeOpenworkRuntimeConfigFile`** generates a runtime configuration file, while **`keepOpenworkRuntimeConfigFileFresh`** registers a file-watcher to persist SQLite runtime DB changes to the OpenCode config.
3. **`resolveOpencodeModelsUrl`** determines the models catalog URL for the engine.
4. **`createManagedOpencodeServer()`** (implemented in [`apps/server/src/managed-opencode.ts`](https://github.com/different-ai/openwork/blob/main/apps/server/src/managed-opencode.ts)) launches the child process with a custom environment containing `OPENWORK_SERVER_URL`, `OPENWORK_SERVER_TOKEN`, `OPENCODE_CONFIG`, and `OPENCODE_MODELS_URL`.
5. The server injects the spawned engine's URL, username, and password into each workspace entry, exposing the OpenCode API as if it were a remote service.
6. **`registerTrustedOpencodeProcess()`** records the child's identity (`managedOpencodeIdentity`) to establish trust for subsequent API calls.

### HTTP Server Startup

Regardless of whether a managed OpenCode child spawns, **`startServer(config)`** (from [`apps/server/src/server.ts`](https://github.com/different-ai/openwork/blob/main/apps/server/src/server.ts)) initializes the HTTP server. This returns a `ServeResult` containing the bound port and a `stop()` method for graceful termination.

### MCP Synchronization

If a managed OpenCode child exists, **`syncAllWorkspacesRuntimeMcpToEngine()`** pushes MCP (model-control-plane) entries from the runtime SQLite database into the engine. This synchronization ensures that model configurations become immediately visible without requiring a manual reload.

### The EmbeddedServerHandle Return Value

The function returns an **`EmbeddedServerHandle`** containing:
- `port` and `url` of the bound HTTP server
- The resolved `config` object
- Information about the managed OpenCode child (`managedOpencodeExecution`, `managedOpencode`)
- A **`stop()`** method that gracefully shuts down the HTTP server, terminates the managed OpenCode child, clears the trusted-process registry, and disposes of the runtime-config watcher

If multiple shutdown steps fail, `stop()` collects all errors and re-throws them as an **`AggregateError`**.

## Code Examples: Starting the Embedded Server

### Starting with a Managed OpenCode Child

Use this configuration to spawn a temporary OpenCode engine automatically:

```typescript
import { startEmbeddedServer } from "openwork-server";

const handle = await startEmbeddedServer({
  host: "127.0.0.1",
  port: 0,                     // 0 ⇒ auto-assign a free port
  token: "my-dev-token",
  workspaces: [{ id: "local", path: "./workspace", workspaceType: "local" }],
  manageOpencode: true,       // spawn a temporary OpenCode engine
  opencodeBin: "./bin/opencode", // optional – defaults to $OPENWORK_OPENCODE_BIN
});

console.log(`Server running at ${handle.url}`);
// → http://127.0.0.1:48321 (example)

// Use the API (e.g., fetch a provider list)
const resp = await fetch(`${handle.url}/runtime-config/providers`, {
  headers: { "x-openwork-host-token": "my-dev-token" },
});
const providers = await resp.json();
console.log(providers);

// Graceful shutdown – safe to call multiple times
await handle.stop();

```

### Connecting to an External OpenCode Engine

Use this approach when running a pre-started OpenCode instance:

```typescript
const handle = await startEmbeddedServer({
  host: "127.0.0.1",
  port: 8080,
  token: "host-token",
  opencodeBaseUrl: "http://127.0.0.1:4000", // pre-started OpenCode instance
  workspaces: [{ id: "demo", path: "./demo-ws", workspaceType: "local" }],
});

await handle.stop(); // stops only the HTTP server

```

## Lifecycle Testing and Error Handling

The test suite in **[`apps/server/src/embedded-lifecycle.test.ts`](https://github.com/different-ai/openwork/blob/main/apps/server/src/embedded-lifecycle.test.ts)** validates the embedded server's robustness across edge cases:

- **Cleanup verification**: Ensures proper resource disposal when the managed OpenCode process fails to become ready
- **Runtime config handling**: Confirms that stopped servers no longer write runtime configuration files
- **Concurrency support**: Tests correct behavior when multiple embedded servers share the same runtime database
- **Idempotent shutdown**: Verifies that `handle.stop()` returns the same promise on repeated calls without crashing
- **Error aggregation**: Validates that shutdown failures in both the HTTP server and managed OpenCode child are combined into a single `AggregateError`

Additional model URL resolution logic is tested in **[`apps/server/src/opencode-models-url.test.ts`](https://github.com/different-ai/openwork/blob/main/apps/server/src/opencode-models-url.test.ts)**.

## Summary

- **OpenWork embedded server mode** runs the HTTP backend in-process via `startEmbeddedServer()` in [`apps/server/src/embedded.ts`](https://github.com/different-ai/openwork/blob/main/apps/server/src/embedded.ts)
- The initialization sequence resolves configuration, prepares workspaces, optionally spawns a managed OpenCode child via `createManagedOpencodeServer()`, starts the HTTP server, and synchronizes MCP entries
- The **`EmbeddedServerHandle`** provides programmatic control over server lifecycle, including graceful shutdown with `AggregateError` support
- **Embedded mode** eliminates separate process management for local development and enables isolated, programmatic server control in automated tests
- Key source files include [`apps/server/src/embedded.ts`](https://github.com/different-ai/openwork/blob/main/apps/server/src/embedded.ts), [`apps/server/src/managed-opencode.ts`](https://github.com/different-ai/openwork/blob/main/apps/server/src/managed-opencode.ts), and [`apps/server/src/embedded-lifecycle.test.ts`](https://github.com/different-ai/openwork/blob/main/apps/server/src/embedded-lifecycle.test.ts)

## Frequently Asked Questions

### What is the difference between embedded mode and running the server separately?

Embedded mode runs the OpenWork HTTP server **in-process** using `startEmbeddedServer()`, allowing you to start and stop the server programmatically within your Node process. Running the server separately requires spawning a distinct Node process and managing it externally. Embedded mode is preferred for automated testing and local development workflows where process isolation adds unnecessary complexity.

### How does the embedded server handle shutdown errors?

The `stop()` method on the `EmbeddedServerHandle` gracefully shuts down all components including the HTTP server, managed OpenCode child process, and file watchers. If multiple shutdown steps encounter errors, the method collects all exceptions and re-throws them as a single **`AggregateError`**, ensuring no failure is silently ignored during cleanup.

### Can I run multiple embedded servers concurrently?

Yes, the embedded server supports concurrent instances. The test suite in [`apps/server/src/embedded-lifecycle.test.ts`](https://github.com/different-ai/openwork/blob/main/apps/server/src/embedded-lifecycle.test.ts) specifically validates scenarios where multiple embedded servers share the same runtime SQLite database. Each server receives its own port allocation and isolated workspace configurations, though you should ensure unique port assignments to prevent binding conflicts.

### How do I connect to an external OpenCode engine instead of the managed one?

Pass the `opencodeBaseUrl` parameter to `startEmbeddedServer()` with the URL of your pre-started OpenCode instance. When this parameter is provided, the embedded server skips the `manageOpencode` initialization logic in [`apps/server/src/embedded.ts`](https://github.com/different-ai/openwork/blob/main/apps/server/src/embedded.ts) and connects directly to the external engine, injecting the provided URL into workspace configurations without spawning a child process.