Workspace KV Store Architecture: How OpenWork Persists Server-Side State

The workspace-kv-store is a generic, SQLite-backed key-value layer that uses a factory pattern with typed serialization, connection pooling via Maps, and dual-runtime support to persist per-workspace state safely without redundant file creation.

The different-ai/openwork repository implements a robust server-side state persistence mechanism through its workspace-kv-store architecture. This system provides a type-safe abstraction over SQLite, storing per-workspace configuration, runtime Opencode settings, and session-group data while balancing flexibility, safety, and performance through lazy initialization and intelligent connection management.

Core Architecture Components

The architecture centers on a factory-based design that decouples storage logic from database implementation details, allowing multiple typed stores to share underlying SQLite connections safely.

Factory Pattern and Type Safety

The createWorkspaceKvStore function in apps/server/src/workspace-kv-store.ts (lines 94-112) serves as the primary entry point. It accepts a table name, value column identifier, and explicit parse/serialize functions to enable storing any JSON-serializable data with full TypeScript type safety.

const workspaceConfigStore = createWorkspaceKvStore<Record<string, unknown>>({
  tableName: "openwork_workspace_configs",
  valueColumn: "config_json",
  parse: (json) => JSON.parse(json) as Record<string, unknown>,
  serialize: (value) => JSON.stringify(value),
});

This factory generates a typed store instance scoped to a specific workspace_id primary key, ensuring that operations remain isolated to individual workspaces while maintaining schema flexibility.

SQLite Schema Configuration

The tableConfig utility (lines 30-55 in workspace-kv-store.ts) dynamically generates SQL statements for table creation, row selection, and upsert operations. It supports an optional schema-version column to facilitate future migrations, using INSERT … ON CONFLICT statements to handle updates atomically.

Connection Management and Caching

Two Map structures declared at the top of workspace-kv-store.ts (lines 13-16) optimize resource utilization:

  • dbByPath: Caches database connections per file path, ensuring multiple stores share a single SQLite connection
  • tableDbByPath: Tracks opened tables to prevent redundant initialization

This dual-layer caching means the first store accessing a runtime DB creates the connection (connectionEntries: 1), while subsequent stores for other tables increment tableEntries without spawning additional file handles.

Runtime Environments and Database Abstraction

The architecture abstracts runtime differences to support both modern and legacy JavaScript environments seamlessly.

Dual Runtime Support

The openTableDb function (lines 66-124) branches on process.versions.bun to select the appropriate database driver. When running on Bun, it uses drizzle-orm; on plain Node, it falls back to node:sqlite. The runtimeDb and openRuntimeSqliteDatabase functions in apps/server/src/runtime-db.ts (lines 23-48) manage lazy database file creation, ensuring the SQLite file only materializes when write operations occur.

Lazy Initialization and Read-Only Access

To prevent unnecessary file creation during startup or sync operations, readableWorkspaceKvDb (lines 126-136) returns a store instance that never creates the database file if it doesn't exist. This read-only variant enables safe inspection of persisted state without side effects, critical for initialization workflows that verify existing configuration before writing.

Public API and Usage Patterns

The workspace-kv-store exposes a consistent async interface for CRUD operations against workspace-scoped data.

Standard CRUD Operations

All store instances provide the following methods (lines 138-173):

  • get(serverConfig, workspaceId): Retrieves and parses JSON payload using the configured parser
  • has(serverConfig, workspaceId): Checks for key existence without deserialization overhead
  • set(serverConfig, workspaceId, value): Serializes data and upserts via atomic SQL operations
  • setSerialized: Allows pre-serialized value insertion for advanced use cases
  • workspaceKvStoreCacheStatsForTests: Exposes diagnostic metrics for connection pool verification

Practical Implementation Example

Derived from the test suite in apps/server/src/workspace-kv-store.test.ts (lines 23-108), here is a complete round-trip implementation:

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

// Define a typed store for generic objects
const recordStore = createWorkspaceKvStore<Record<string, unknown>>({
  tableName: "workspace_kv_factory_round_trip",
  valueColumn: "config_json",
  parse: (json) => JSON.parse(json) as Record<string, unknown>,
  serialize: (value) => JSON.stringify(value),
});

// Persist workspace configuration
await recordStore.set(serverConfig, "ws_123", { enabled: true });

// Retrieve persisted state
const persisted = await recordStore.get(serverConfig, "ws_123");
// Result: { enabled: true }

The architecture gracefully handles corruption: if JSON parsing fails (due to manual file manipulation or disk errors), the store returns an empty object {} rather than throwing, ensuring application stability during unexpected data states.

Summary

  • Factory-based typed stores: createWorkspaceKvStore generates type-safe instances with custom serialization logic, implemented in apps/server/src/workspace-kv-store.ts (lines 94-112)
  • Connection pooling: dbByPath and tableDbByPath Maps cache database connections and table handles to minimize file descriptor usage (lines 13-16)
  • Runtime abstraction: Dual support for Bun (drizzle-orm) and Node (node:sqlite) via runtime detection in openTableDb (lines 66-124)
  • Safe read operations: readableWorkspaceKvDb prevents file creation during reads, essential for startup synchronization (lines 126-136)
  • Atomic upserts: All write operations use INSERT … ON CONFLICT SQL patterns for safe concurrent updates
  • Resilient parsing: Malformed JSON returns empty objects rather than crashing the application

Frequently Asked Questions

How does workspace-kv-store handle concurrent access to the same SQLite file?

The architecture uses dbByPath to maintain a single connection per SQLite file path, ensuring that multiple store instances or simultaneous operations share one underlying database connection. This prevents file locking conflicts while the atomic INSERT … ON CONFLICT SQL statements guarantee transactional safety for write operations.

What is the difference between readableWorkspaceKvDb and the standard store?

readableWorkspaceKvDb (lines 126-136) returns a KV store instance that explicitly never creates the SQLite database file if it doesn't exist, making it ideal for read-only startup checks. Standard store operations through createWorkspaceKvStore will lazily initialize the database file and tables upon the first write operation via set() or setSerialized().

Why does the factory require both parse and serialize functions?

The createWorkspaceKvStore factory (lines 94-112) accepts explicit parse and serialize parameters to support type-safe storage of complex JSON structures while maintaining zero dependencies on specific validation libraries. This design allows developers to inject custom parsers (e.g., Zod, Valibot) or handle versioning during deserialization without modifying the core storage layer.

How does the architecture support database migrations?

The tableConfig utility (lines 30-55) includes an optional schema-version column in generated SQL statements. When creating stores, developers can specify schema versions, allowing future migrations to detect legacy table structures and execute ALTER statements accordingly. The factory pattern ensures each table migration logic remains encapsulated within its respective store definition.

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 →