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

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) handles cross-platform SQLite connectivity and file path resolution. Above this sits the Workspace-KV Store (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), 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)

The foundation of OpenWork's state management resides in 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.

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

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

Layer 2: Workspace-KV Store (workspace-kv-store.ts)

Built atop the runtime database, 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.

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)

The highest abstraction layer in 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.

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 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 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 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 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 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.

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 →