# How OpenWork Manages Project State: A Three-Layer SQLite Architecture

> Discover how OpenWork manages project state with a three-layer SQLite architecture. Learn about type-safe key-value storage for Bun and Node.js.

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

---

**OpenWork centralizes project state management in a single SQLite file using a three-layer abstraction that supports both Bun and Node.js runtimes while providing type-safe key-value storage per workspace.**

The different-ai/openwork repository implements a sophisticated strategy for managing mutable runtime data through a local SQLite database. This architecture ensures data consistency across different JavaScript environments while exposing a clean, type-safe API for workspace-specific configuration and global engine settings.

## The Three-Layer State Architecture

OpenWork structures its state management into three distinct layers that abstract database complexity from application logic. The **Runtime Database** layer ([`runtime-db.ts`](https://github.com/different-ai/openwork/blob/main/runtime-db.ts)) handles cross-platform SQLite connectivity and file path resolution. Above this sits the **Workspace-KV Store** ([`workspace-kv-store.ts`](https://github.com/different-ai/openwork/blob/main/workspace-kv-store.ts)), which provides generic, typed key-value tables for each workspace. The top layer, **Runtime Opencode Config** ([`runtime-opencode-config-store.ts`](https://github.com/different-ai/openwork/blob/main/runtime-opencode-config-store.ts)), manages the specific configuration that drives the OpenCode engine, handling merging between global defaults and workspace-specific overrides.

## Layer 1: Runtime Database ([`runtime-db.ts`](https://github.com/different-ai/openwork/blob/main/runtime-db.ts))

The foundation of OpenWork's state management resides in [`dev/apps/server/src/runtime-db.ts`](https://github.com/different-ai/openwork/blob/main/dev/apps/server/src/runtime-db.ts), which determines the database location and opens the correct SQLite driver based on the runtime environment.

**Path resolution** occurs through `runtimeDbPath(config)`, returning a location derived from server configuration or environment overrides. The system stores data in a file named `runtime.sqlite` beside the user's configuration directory.

**Cross-runtime support** is achieved by detecting `process.versions.bun` at startup. When running under Bun, the system imports from `bun:sqlite` and uses `drizzle-orm/bun-sqlite`; otherwise, it falls back to `node:sqlite`. The `openRuntimeSqliteDatabase(path)` function lazily initializes the connection, calling `ensureDir` to create parent directories before returning a unified `RuntimeSqliteDatabase` object with a standard `close` method.

```typescript
import { openRuntimeSqliteDatabase, runtimeDbPath } from "./runtime-db.js";

const db = await openRuntimeSqliteDatabase(runtimeDbPath(serverConfig));

```

## Layer 2: Workspace-KV Store ([`workspace-kv-store.ts`](https://github.com/different-ai/openwork/blob/main/workspace-kv-store.ts))

Built atop the runtime database, [`dev/apps/server/src/workspace-kv-store.ts`](https://github.com/different-ai/openwork/blob/main/dev/apps/server/src/workspace-kv-store.ts) provides a generic key-value interface that automatically schemas tables per workspace and caches connections to prevent resource leaks.

**Dynamic schema creation** happens via `createTableSql`, which generates tables containing `workspace_id`, a JSON value column (configurable as `valueJson` or custom names like `settings_json`), optional extra columns, and an `updated_at` timestamp.

**Connection caching** is critical for performance. The module maintains two internal Maps—`dbByPath` and `tableDbByPath`—ensuring each physical database file and each table within those files are opened only once per process. This prevents connection exhaustion during high-frequency operations.

**Type-safe API** is exposed through `createWorkspaceKvStore<T>(options)`, which returns an object with `get`, `set`, `has`, `setSerialized`, and `serialize` methods. These methods work identically across Bun and Node backends, accepting `serverConfig` and `workspaceId` parameters to scope operations.

```typescript
import { createWorkspaceKvStore } from "./workspace-kv-store.js";

type Settings = { theme: string; notifications: boolean };
const settingsStore = createWorkspaceKvStore<Settings>({
  tableName: "workspace_settings",
  valueColumn: "settings_json",
  parse: JSON.parse,
  serialize: JSON.stringify,
});

await settingsStore.set(serverConfig, workspaceId, { theme: "dark", notifications: true });
const settings = await settingsStore.get(serverConfig, workspaceId);

```

## Layer 3: Runtime Opencode Config Store ([`runtime-opencode-config-store.ts`](https://github.com/different-ai/openwork/blob/main/runtime-opencode-config-store.ts))

The highest abstraction layer in [`dev/apps/server/src/runtime-opencode-config-store.ts`](https://github.com/different-ai/openwork/blob/main/dev/apps/server/src/runtime-opencode-config-store.ts) manages the `RuntimeOpencodeConfig` domain model, which includes default agents, plugins, disabled providers, MCP endpoints, and permission overrides.

**Global versus workspace scoping** is handled through a special workspace ID `__openwork_engine_global__` that stores system-wide defaults. The `readEffectiveRuntimeOpencodeConfig(serverConfig, workspaceId)` function merges global and workspace-specific rows from the `runtime_opencode_configs` table, with workspace values taking precedence over global defaults. This shallow-merge strategy deduplicates lists while preserving configuration inheritance.

**Reactive updates** are supported via `onRuntimeOpencodeConfigWrite`, which registers listeners that trigger when configuration changes occur. This allows subsystems to regenerate derived files or reload settings without polling.

**Safety validation** is enforced by `inspectRuntimeOpencodeConfigState`, which reads configuration rows without creating the database and enforces size and depth limits to prevent processing of malformed data.

```typescript
import { readEffectiveRuntimeOpencodeConfig } from "./runtime-opencode-config-store.js";

const effectiveConfig = await readEffectiveRuntimeOpencodeConfig(serverConfig, workspaceId);
console.log(effectiveConfig.plugin); // → list of enabled plugins

```

## State Management Flow

Understanding how these layers interact clarifies the project state lifecycle in OpenWork:

1. **Startup**: [`server.ts`](https://github.com/different-ai/openwork/blob/main/server.ts) initializes the system by calling `runtimeDbPath` to locate `runtime.sqlite` and `openRuntimeSqliteDatabase` to establish the connection, selecting the appropriate driver based on the runtime environment.

2. **Workspace creation**: When a new workspace is provisioned, the system invokes `createWorkspaceKvStore` factories to generate dedicated tables with the proper schema, caching the connection for reuse.

3. **Config updates**: Application code calls `writeRuntimeOpencodeConfig`, which serializes the `RuntimeOpencodeConfig` object as JSON, updates the `runtime_opencode_configs` table, and notifies any registered listeners through the `onRuntimeOpencodeConfigWrite` mechanism.

4. **Read-through**: Consumers access merged configuration states via `readEffectiveRuntimeOpencodeConfig`, which transparently combines global defaults from `__openwork_engine_global__` with workspace-specific overrides.

## Summary

- OpenWork persists all mutable state in a single SQLite file (`runtime.sqlite`) located in the user's configuration directory
- The [`runtime-db.ts`](https://github.com/different-ai/openwork/blob/main/runtime-db.ts) module automatically detects Bun via `process.versions.bun` and loads `bun:sqlite` or falls back to `node:sqlite` for cross-platform compatibility
- Workspace-specific data is managed through type-safe KV stores created via `createWorkspaceKvStore`, with connection pooling via `dbByPath` and `tableDbByPath` Maps
- Configuration follows a layered inheritance model where global defaults stored under `__openwork_engine_global__` merge with workspace-specific values
- The system supports reactive updates through `onRuntimeOpencodeConfigWrite` listeners and validates data integrity via `inspectRuntimeOpencodeConfigState`

## Frequently Asked Questions

### Where does OpenWork store project state?

All mutable runtime state persists in a single SQLite database file named `runtime.sqlite` located in the user's configuration directory. The `runtimeDbPath()` function in [`runtime-db.ts`](https://github.com/different-ai/openwork/blob/main/runtime-db.ts) resolves this location based on server configuration or environment variables, ensuring the database lives alongside other user-specific application data.

### How does OpenWork handle different JavaScript runtimes?

The [`runtime-db.ts`](https://github.com/different-ai/openwork/blob/main/runtime-db.ts) module detects the execution environment by checking for the presence of `process.versions.bun`. When running under Bun, it imports from `bun:sqlite` and uses `drizzle-orm/bun-sqlite`; otherwise, it falls back to the standard `node:sqlite` package. This abstraction allows the same codebase to operate efficiently on both runtimes without conditional logic scattered throughout the application.

### What is the difference between global and workspace-specific configuration?

OpenWork uses a special workspace ID `__openwork_engine_global__` to store system-wide defaults in the `runtime_opencode_configs` table. When retrieving configuration via `readEffectiveRuntimeOpencodeConfig()`, the function performs a shallow merge of global settings with workspace-specific rows, with workspace values taking precedence. This enables inheritance patterns where individual workspaces can override specific settings while retaining global defaults for unspecified values.

### How does the KV store prevent duplicate database connections?

The [`workspace-kv-store.ts`](https://github.com/different-ai/openwork/blob/main/workspace-kv-store.ts) module maintains two internal caches: `dbByPath` and `tableDbByPath`. These Maps ensure that each physical database file and each table within those files are opened only once per process. When `createWorkspaceKvStore()` is called, it returns a cached instance if one exists, preventing resource leaks and connection exhaustion during high-frequency read/write operations.