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:
- Standard load: Parse
openwork-workspaces.jsonif present - Recovery: Attempt to reconstruct workspaces from token store or server configuration
- Initialization: Return
EMPTY_WORKSPACE_LISTas 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:
- Checks for existence of
openwork-workspaces.json - If missing, copies
workspace-state.jsonto the new location - Subsequent launches use the migrated file
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
Related Files in the Codebase
| 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.jsonin Electron'suserDatadirectory - 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →