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

> Discover how OpenWork Desktop persists workspace state using workspace-store.mjs and atomic writes to a single JSON file, ensuring data integrity and preventing corruption.

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

---

**The OpenWork desktop client persists workspace state to a single JSON file ([`openwork-workspaces.json`](https://github.com/different-ai/openwork/blob/main/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](https://github.com/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:

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

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

```js
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](https://github.com/different-ai/openwork/blob/dev/apps/desktop/electron/workspace-store.mjs#L110-L132).

---

## Writing State with Legacy Compatibility

`writeWorkspaceState` persists mutations while duplicating fields for backward compatibility:

```js
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](https://github.com/different-ai/openwork/blob/dev/apps/desktop/electron/workspace-store.mjs#L91-L106).

---

## Migration from Legacy Electron State

Older Electron versions stored workspace state in [`workspace-state.json`](https://github.com/different-ai/openwork/blob/main/workspace-state.json). The `migrateLegacyElectronWorkspaceStateIfNeeded` function performs a one-time migration when the new file is absent:

- Checks for existence of [`openwork-workspaces.json`](https://github.com/different-ai/openwork/blob/main/openwork-workspaces.json)
- If missing, copies [`workspace-state.json`](https://github.com/different-ai/openwork/blob/main/workspace-state.json) to 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](https://github.com/different-ai/openwork/blob/dev/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](https://github.com/different-ai/openwork/blob/dev/apps/desktop/electron/workspace-store.mjs#L1204-L1229).

---

## Practical Usage Examples

### Initialize the workspace store

```js
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

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

```

### Read current workspaces

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

```

### Switch active workspace

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

```

### Full state reset (debugging)

```js
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`](https://github.com/different-ai/openwork/blob/main/packages/paths/src/index.ts) | Path constants (`desktopBootstrapPath`, etc.) |
| [`apps/desktop/electron/main.ts`](https://github.com/different-ai/openwork/blob/main/apps/desktop/electron/main.ts) | IPC setup and store injection |

---

## Summary

- **Storage location**: [`openwork-workspaces.json`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/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](https://github.com/different-ai/openwork) repository, with the main persistence functions concentrated between lines 81-132 and the public API exported at lines 1204-1229.