OpenWork Migration System for Profile and Workspace Upgrades: A Technical Deep Dive

The OpenWork migration system automatically upgrades developer profiles and workspace configurations through lazy, pure-data transformations triggered at server boot or first config read.

OpenWork manages persistent user state across two distinct layers: developer profiles that encapsulate per-instance runtime data, and workspace configurations that define project settings. When new releases introduce schema changes, the OpenWork migration system ensures seamless upgrades without manual intervention. This article examines the implementation details, source code paths, and exact behavior of both migration paths based on the different-ai/openwork repository.

How Profile Migrations Work in OpenWork

Profile migrations handle structural changes to the runtime SQLite database and associated per-profile state. Each development instance runs under a specific profile—whether the default profile, one created via pnpm dev:worktree, or a custom-named profile. The profile folder stores the runtime.sqlite database, credential caches, and workspace lock files.

Detecting Schema Version at Startup

The migration detection begins in dev/ee/packages/den-db/scripts/bootstrap.ts. When the server starts, the bootstrap script checks PRAGMA user_version against the expected schema version:

// From dev/ee/packages/den-db/scripts/bootstrap.ts
// The bootstrap script runs automatically when starting any profile
await migrate(db, { migrationsFolder });

If user_version lags behind the current version, the system sequentially applies SQL migration files from dev/ee/packages/den-db/drizzle/. This Drizzle-managed folder contains all DDL and DML statements required to bring older profiles forward.

Automatic Migration Application

The profile migration process follows these steps:

  1. Baseline detection – The script logs "recording X committed migrations as baseline" to establish the starting state
  2. Sequential application – Each pending migration file executes in order, altering tables, adding columns, or migrating data as needed
  3. Version advancement – PRAGMA user_version updates automatically, preventing re-application

This entire process runs without user interaction. Simply starting a profile—whether through pnpm dev for the default profile or pnpm dev:worktree for a stable worktree profile—triggers any necessary migrations.


# Example: Starting a profile that automatically runs pending DB migrations

pnpm dev:worktree  # Creates stable profile folder and applies migrations if needed

How Workspace Configuration Migration Works

Workspace configuration presents a distinct migration challenge. Legacy OpenWork installations stored workspace settings in a JSON file at .opencode/openwork.json. Modern versions store this data in the workspace_config table of runtime.sqlite. The OpenWork migration system handles this transition through lazy on-read migration.

The On-Read Migration Pattern

The migration triggers when any code calls readOpenworkWorkspaceConfig() from apps/server/src/openwork-workspace-config-store.ts. This typically happens via the GET /workspace/:id/config endpoint:

// From apps/server/src/openwork-workspace-config-store.ts
// Triggers automatically on first config read
const config = await readOpenworkWorkspaceConfig(serverConfig, workspaceId);

The function implements the following detection logic:

  1. Query the workspace_config table for the workspace ID
  2. If found, return the DB-stored configuration
  3. If missing, check for legacy .opencode/openwork.json on disk
  4. If legacy file exists, parse JSON and insert into workspace_config table
  5. Return the configuration (now sourced from DB for future reads)

Backward Compatibility Guarantees

The workspace migration preserves backward compatibility deliberately. The legacy JSON file remains untouched on disk—no deletion occurs. Existing workspaces continue functioning without modification, while new workspaces write directly to the database. The API response shape remains identical, including fields like openwork.version, authorizedRoots, and workspace preset information.

// Example: Reading workspace config triggers migration if needed
import { readOpenworkWorkspaceConfig } from "./openwork-workspace-config-store.js";

const workspaceId = "ws_legacy_fixture";
const config = await readOpenworkWorkspaceConfig(serverConfig, workspaceId);

// If legacy openwork.json existed, it has now been copied into DB
console.log(config.version);          // → 1
console.log(config.authorizedRoots);  // → ["/absolute/path/to/workspace"]

Migration Safety and Testing

Both migration paths operate as pure-data transformations—no arbitrary code executes in the user environment. The system includes comprehensive test coverage to verify correctness.

Profile Migration Verification

Profile migrations rely on Drizzle's established migration framework, with SQL files version-controlled in ee/packages/den-db/drizzle/. Each migration is idempotent and transactional where possible.

Workspace Migration Test Implementation

The end-to-end test in apps/server/src/openwork-config-db-migration.e2e.test.ts validates the complete JSON-to-DB migration flow. This test proves:

  • Legacy openwork.json files are correctly detected and parsed
  • Database rows are created with expected fields (version = 1, authorizedRoots, workspace.preset)
  • API responses maintain identical payload shapes post-migration
  • Subsequent reads use the database as source of truth

Key Implementation Files

File Purpose
dev/ee/packages/den-db/scripts/bootstrap.ts Bootstraps runtime DB and executes profile migrations at startup
dev/ee/packages/den-db/drizzle/ Directory containing all SQL migration files for profile schema changes
apps/server/src/openwork-workspace-config-store.ts Runtime implementation of workspace config reads and lazy migration
apps/server/src/openwork-config-db-migration.e2e.test.ts End-to-end test verifying legacy JSON to DB migration behavior

Summary

  • Profile migrations run automatically at server startup, detecting schema version via PRAGMA user_version and applying SQL files from the Drizzle migration folder
  • Workspace config migrations trigger lazily on first read through readOpenworkWorkspaceConfig(), transparently moving data from .opencode/openwork.json to the workspace_config table
  • Both migrations are non-destructive and backward-compatible, preserving existing files and maintaining API response shapes
  • The system is pure-data—no user code execution occurs during upgrades
  • Extensive testing in openwork-config-db-migration.e2e.test.ts guarantees migration safety

Frequently Asked Questions

What triggers OpenWork profile migrations?

Profile migrations trigger automatically when starting any OpenWork server instance. The bootstrap script in dev/ee/packages/den-db/scripts/bootstrap.ts runs PRAGMA user_version against runtime.sqlite and applies pending migrations from ee/packages/den-db/drizzle/ if the database schema is outdated. No manual command is required—simply running pnpm dev or pnpm dev:worktree executes any needed upgrades.

Will my existing workspace configurations break when upgrading OpenWork?

No. The OpenWork migration system specifically preserves backward compatibility for workspace configurations. Legacy .opencode/openwork.json files remain on disk and functional. The system only copies data into the database on first read, then uses the DB as the source of truth for subsequent accesses. The API response format remains identical, ensuring no client-side changes are needed.

What happens if a migration fails?

Profile migrations run through Drizzle's transactional migration system, which applies changes atomically where SQLite semantics allow. Failed migrations surface as startup errors, preventing the server from booting with an inconsistent state. Workspace config migrations operate as single-row insertions after parsing JSON—if parsing fails, the original error propagates without partial database writes.

How do I create a development profile that persists across upgrades?

Use pnpm dev:worktree instead of pnpm dev. This command creates a stable profile folder with its own runtime.sqlite database and workspace lock. The profile survives git branch switches and OpenWork version upgrades, with automatic migrations applied whenever schema changes occur.

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 →