How OpenWork Desktop Persists Workspace State with `workspace-store.mjs`

The OpenWork desktop client persists workspace state to a single JSON file (openwork-workspaces.json) in Electron's user-data directory, using atomic writes with temporary files and rename operations to prevent corruption.

OpenWork is an open-source desktop application for managing development workspaces. At the heart of its data layer sits workspace-store.mjs, a Node.js module that handles all on-disk persistence for workspace configurations. This article examines exactly how the module writes, reads, migrates, and recovers workspace state—complete with function signatures and code paths from the different-ai/openwork repository.


Where Workspace State Is Stored

The workspaceStatePath() function constructs the absolute path to the state file using Electron's app.getPath("userData") API:

function workspaceStatePath() {
  return path.join(app.getPath("userData"), "openwork-workspaces.json");
}

By default, this resolves to platform-specific locations:

  • macOS: ~/Library/Application Support/OpenWork/openwork-workspaces.json
  • Windows: %APPDATA%/OpenWork/openwork-workspaces.json
  • Linux: ~/.config/OpenWork/openwork-workspaces.json

The path is defined at apps/desktop/electron/workspace-store.mjs#L167-L169.


Atomic Write Strategy

workspace-store.mjs guarantees data integrity through atomic file operations. The writeJsonFileAtomic helper writes to a temporary file first, then renames it into place:

async function writeJsonFileAtomic(outputPath, value) {
  const content = `${JSON.stringify(value, null, 2)}\n`;
  await mkdir(path.dirname(outputPath), { recursive: true });
  const tempPath = `${outputPath}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
  await writeFile(tempPath, content, "utf8");
  await rename(tempPath, outputPath);
}

This pattern ensures that openwork-workspaces.json never exists in a partially-written state. Even if the process crashes mid-write, the temp file is orphaned and the original remains intact. The implementation lives at apps/desktop/electron/workspace-store.mjs#L81-L88.


Reading and Normalizing Workspace State

The readWorkspaceState function loads existing state or bootstraps a fresh configuration. It handles three scenarios:

  1. Standard load: Parse openwork-workspaces.json if present
  2. Recovery: Attempt to reconstruct workspaces from token store or server configuration
  3. Initialization: Return EMPTY_WORKSPACE_LIST as fallback
async function readWorkspaceState() {
  const workspaceStateExists = existsSync(workspaceStatePath());
  const state = await readJsonFile(workspaceStatePath(), EMPTY_WORKSPACE_LIST);
  // Field normalization for backward compatibility…
}

The function normalizes legacy field names including selectedId, selectedWorkspaceId, and activeId to maintain compatibility across Electron builds. Recovery logic via recoverWorkspacesFromKnownState is triggered when the state file is missing. See apps/desktop/electron/workspace-store.mjs#L110-L132.


Writing State with Legacy Compatibility

writeWorkspaceState persists mutations while duplicating fields for backward compatibility:

async function writeWorkspaceState(nextState) {
  const outputPath = workspaceStatePath();
  const selectedId = String(nextState?.selectedId ?? nextState?.activeId ?? "");
  const watchedId = typeof nextState?.watchedId === "string" ? nextState.watchedId : "";
  const output = {
    ...nextState,
    selectedId,
    selectedWorkspaceId: selectedId,      // legacy alias
    watchedId: watchedId || null,
    watchedWorkspaceId: watchedId,        // legacy alias
    activeId: selectedId || null,         // legacy alias
  };
  await writeJsonFileAtomic(outputPath, output);
  return output;
}

The duplicate keys (selectedWorkspaceId, watchedWorkspaceId, activeId) allow older OpenWork builds to read state written by newer versions. This function is located at apps/desktop/electron/workspace-store.mjs#L91-L106.


Migration from Legacy Electron State

Older Electron versions stored workspace state in workspace-state.json. The migrateLegacyElectronWorkspaceStateIfNeeded function performs a one-time migration when the new file is absent:

This ensures seamless upgrades without data loss. The migration logic appears at apps/desktop/electron/workspace-store.mjs#L84-L98.


Complete Persistence API

workspace-store.mjs exports a unified interface consumed by the Electron main process. The key functions are:

Function Purpose
readWorkspaceState() Load workspace list with automatic recovery
writeWorkspaceState(nextState) Persist state atomically with legacy aliases
createWorkspace(options) Add local workspace and persist
createRemoteWorkspace(options) Add remote workspace and persist
setSelectedWorkspace(id) Update active workspace ID
setRuntimeActiveWorkspace(id) Set runtime-only active workspace
resetOpenworkState() Delete state files for clean reset

These exports are defined at apps/desktop/electron/workspace-store.mjs#L1204-L1229.


Practical Usage Examples

Initialize the workspace store

import { createWorkspaceStore } from "./workspace-store.mjs";

const store = createWorkspaceStore({
  app,                                   // Electron app instance
  defaultDenBaseUrl: "https://api.openworklabs.com",
  defaultRequireSignin: false,
  forceRequireSignin: false,
});

Create and persist a local workspace

await store.createWorkspace({
  folderPath: "~/my-project",
  name: "My Project",
  preset: "starter",
});
// Automatically writes to openwork-workspaces.json

Read current workspaces

const state = await store.readWorkspaceState();
console.log(state.workspaces.map(w => w.name));
// Output: ['My Project', 'Team Backend', 'Docs Site']

Switch active workspace

const targetId = state.workspaces[0].id;
await store.setSelectedWorkspace(targetId);

Full state reset (debugging)

await store.resetOpenworkState();
// Deletes openwork-workspaces.json and bootstrap configuration

File Role
apps/desktop/electron/workspace-store.mjs Core persistence logic
apps/desktop/electron/remote-workspace.mjs Remote workspace discovery
apps/desktop/electron/workspace-archive.mjs Export/import capabilities
packages/paths/src/index.ts Path constants (desktopBootstrapPath, etc.)
apps/desktop/electron/main.ts IPC setup and store injection

Summary

  • Storage location: openwork-workspaces.json in Electron's userData directory
  • Atomic writes: Temp-file pattern with rename() prevents corruption
  • Backward compatibility: Legacy field aliases maintained in every write
  • Recovery path: Falls back to token store or server config if state missing
  • Migration support: Automatic upgrade from workspace-state.json

Frequently Asked Questions

What happens if the workspace state file is corrupted?

readWorkspaceState catches parsing errors and falls back to recoverWorkspacesFromKnownState, which attempts to reconstruct the workspace list from authentication tokens or server configuration. If recovery fails, it returns an empty workspace list that the user can repopulate.

Can multiple OpenWork instances write to the same state file safely?

The atomic write pattern using process-unique temporary filenames (${outputPath}.${process.pid}.${randomBytes(6).toString("hex")}.tmp) minimizes collision risk, but concurrent access from multiple processes is not explicitly synchronized. For typical single-user desktop usage, this is sufficient.

Why does OpenWork duplicate field names like selectedId and selectedWorkspaceId?

These aliases maintain read-compatibility with older Electron builds that expect different property names. When the file format evolved, the team chose to write all known variants rather than force a breaking migration, ensuring seamless downgrades if needed.

Where is the workspace-store.mjs source code located?

The module resides at apps/desktop/electron/workspace-store.mjs in the different-ai/openwork repository, with the main persistence functions concentrated between lines 81-132 and the public API exported at lines 1204-1229.

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 →