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

> Discover the workspace KV store architecture for robust server-side state persistence. Learn how OpenWork uses SQLite, typed serialization, and connection pooling for efficient data management.

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

---

**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`](https://github.com/different-ai/openwork/blob/main/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.

```typescript
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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/apps/server/src/workspace-kv-store.test.ts) (lines 23-108), here is a complete round-trip implementation:

```typescript
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`](https://github.com/different-ai/openwork/blob/main/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.