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 connectiontableDbByPath: 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 parserhas(serverConfig, workspaceId): Checks for key existence without deserialization overheadset(serverConfig, workspaceId, value): Serializes data and upserts via atomic SQL operationssetSerialized: Allows pre-serialized value insertion for advanced use casesworkspaceKvStoreCacheStatsForTests: 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:
createWorkspaceKvStoregenerates type-safe instances with custom serialization logic, implemented inapps/server/src/workspace-kv-store.ts(lines 94-112) - Connection pooling:
dbByPathandtableDbByPathMaps 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 inopenTableDb(lines 66-124) - Safe read operations:
readableWorkspaceKvDbprevents file creation during reads, essential for startup synchronization (lines 126-136) - Atomic upserts: All write operations use
INSERT … ON CONFLICTSQL 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →