Skip to content

Migrate server + desktop state to a local SQLite DB#1999

Draft
src-opn wants to merge 3 commits into
devfrom
server-state-to-db
Draft

Migrate server + desktop state to a local SQLite DB#1999
src-opn wants to merge 3 commits into
devfrom
server-state-to-db

Conversation

@src-opn
Copy link
Copy Markdown
Collaborator

@src-opn src-opn commented May 29, 2026

Summary

Introduces @openwork/desktop-db (Drizzle + SQLite) as the single source of truth for OpenWork-owned state on the user's machine, and ports the server + desktop shell to use it. The driver is runtime-adaptive: better-sqlite3 on Node/Electron, bun:sqlite under the bun-run server. Every table uses TypeID ids via a desktop-local registry (separate from the cloud ee/utils one).

This is the first phase of consolidating the scattered JSON/JSONL/localStorage state documented in the research deep-dive.

What's migrated to the DB

All with: one-time import, .pre-db.bak snapshots (source files are preserved for revert — nothing is deleted), and a migration_state fingerprint guard so we never re-import on every start.

State Was Now (table)
Server config + workspace registry + authorizedRoots server.json server_config, workspace, authorized_root
Scoped API tokens (hashed) tokens.json token
Audit log audit/*.jsonl audit
Electron workspace list + selection openwork-workspaces.json workspace + preference
Per-workspace server tokens openwork-server-tokens.json workspace_server_token
Preferred ports openwork-server-state.json workspace_port
Renderer "real state" prefs (model prefs, drafts, shell-config, onboarding flags, …) localStorage preference (boot hydrate + write-through mirror; web unchanged)

The DB lives next to server.json (<configDir>/openwork.db) so server + Electron + renderer share one file.

Not migrated yet (deferred)

  • opencode.json / opencode.jsonc (MCP servers, plugins, providers, permissions) — needs the OPENCODE_CONFIG_CONTENT vs generated-file strategy since OpenCode reads it at runtime.
  • env.json (user API keys) — blocked on cross-shell read constraint + at-rest encryption decision.
  • .opencode/openwork.json sections (workspace metadata, blueprint sessions, desktop cloud sync) — tables exist, not wired.
  • Google Workspace OAuth vault — table exists, not wired.

Skills / commands / agents stay as files (OpenCode + the reload watcher depend on them).

Notes / caveats

  • SERVER-group renderer prefs (model prefs, hidden models, extension flags, onboarding acks) now live durably in the per-device DB. True cross-device sharing would need server API endpoints — a clean follow-up.
  • Stopped writing openwork-workspaces.json (DB-only; Tauri rollback would lose that state — accepted).

Tests run

  • @openwork/desktop-db: bun test10 pass / 0 fail (typeid, client, Phase 1 + desktop import once, .pre-db.bak/idempotency, preference mirror). typecheck + tsup build clean; drizzle-kit generate clean.
  • apps/server: bun test src/160 pass / 6 fail. The 6 failures are pre-existing on the base branch (verified by stashing): 4 workspace import preview count assertions + 2 seedOpencodeSessionMessages (bun can't load better-sqlite3 for OpenCode's own DB). pnpm typecheck clean.
  • apps/desktop: node --test runtime + remote-workspace → 13 pass / 0 fail; check:electron (50 methods), typecheck:electron, node --check on all .mjs — clean.
  • apps/app: pnpm typecheck clean; pnpm build (Vite) clean.
  • End-to-end smoke tests (real Node + better-sqlite3 + built package): desktop import of the three JSON files + read-back; preference set/get/remove with mirrored-key filtering.

No video/screenshots: this is server/desktop plumbing with no user-visible UI change in this pass. Reviewer repro: run the test commands above.

Introduce @openwork/desktop-db (Drizzle + SQLite, runtime-adaptive between
better-sqlite3 on Node/Electron and bun:sqlite under the server) as the single
source of truth for OpenWork-owned state on the user's machine. Every table uses
TypeID ids via a desktop-local registry (separate from the cloud ee/utils one).

Migrated to the DB (with one-time import, .pre-db.bak snapshots so source files
are preserved for revert, and a migration_state fingerprint guard so we never
re-import on every start):
- server.json: server config + workspace registry + authorizedRoots
- tokens.json: scoped API tokens (hashed)
- audit/*.jsonl: audit log
- Electron openwork-workspaces.json: workspace list + selection
- Electron openwork-server-tokens.json: per-workspace server tokens
- Electron openwork-server-state.json: preferred ports
- Renderer "real state" localStorage keys: model prefs, drafts, shell-config,
  onboarding flags, etc. via boot hydration + write-through mirror (web unchanged)

Not yet migrated (deferred): opencode.json/jsonc (MCP/plugins/providers), env.json,
.opencode/openwork.json sections, Google Workspace OAuth vault. Skills/commands/
agents stay as files (OpenCode + reload watcher depend on them).
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openwork-app Ready Ready Preview, Comment May 29, 2026 2:21am
openwork-den Ready Ready Preview, Comment May 29, 2026 2:21am
openwork-den-worker-proxy Ready Ready Preview, Comment May 29, 2026 2:21am
openwork-landing Ready Ready Preview, Comment, Open in v0 May 29, 2026 2:21am

- env.json -> env_var table. EnvService is now DB-backed (same /env API, key +
  reserved-prefix policy preserved). The desktop shell injects user env vars from
  the DB into process.env before starting the in-process server and managed
  OpenCode (buildChildEnv), so the env.json file is no longer read. Stored
  plaintext for now (parity with the prior 0600 file).
- desktop-bootstrap.json -> preference rows. Electron get/set and the renderer
  bootstrap read it from the DB; the server now surfaces denBaseUrl/denApiBaseUrl
  in ServerConfig from the DB.

Both files are imported once (gated by migration_state) and preserved as
.pre-db.bak snapshots. Bootstrap URLs store "" for unset (preference.value is
NOT NULL JSON).
@src-opn
Copy link
Copy Markdown
Collaborator Author

src-opn commented May 29, 2026

Update: env vars + desktop bootstrap config migrated to the DB (commit 3fb510c)

Two more file-based stores moved into the SQLite DB:

env.json (user API keys) — file-less now

  • EnvService is DB-backed (env_var table). Same /env API; key validation + reserved OPENWORK_/OPENCODE_ policy preserved.
  • The desktop shell now injects user env vars from the DB into process.env before starting the in-process server and managed OpenCode (buildChildEnv), so env.json is no longer read at runtime.
  • Stored plaintext for now (parity with the prior 0600 file); at-rest encryption is a separate follow-up.

desktop-bootstrap.json (cloud/Den config) — DB-backed

  • baseUrl / apiBaseUrl / requireSignin move to preference rows. Electron get/set + the renderer bootstrap read from the DB.
  • The server now surfaces denBaseUrl / denApiBaseUrl in ServerConfig (read from the DB), so it can consume the cloud URLs.

Both files are imported once (gated by migration_state) and preserved as .pre-db.bak snapshots.

Tests

  • @openwork/desktop-db: bun test11 pass / 0 fail (added env-store + bootstrap import/round-trip test).
  • apps/server: bun test src/150 pass / 6 fail — same 6 pre-existing failures as before (seedOpencodeSessionMessages x2, workspace import preview x4); env tests rewritten to DB-backed (env-file + env-routes now green). pnpm typecheck clean.
  • apps/desktop: runtime tests 13/13; typecheck:electron, check:electron, node --check clean.
  • apps/app: pnpm typecheck clean.
  • E2E smoke (real Node + better-sqlite3 + built package): env injection with reserved-key stripping; bootstrap import + round-trip incl. apiBaseUrl: null; .pre-db.bak snapshots created.

Still deferred: opencode.json/jsonc, .opencode/openwork.json sections, Google Workspace OAuth vault.

Drop the .pre-db.bak snapshots. They copied/renamed source files, which broke
rollback to a pre-DB app version that still reads the originals in place.

New model:
- Source files are NEVER modified, copied, renamed, or deleted.
- migration_state records source path + a non-cryptographic content hash.
- A source is imported AT MOST ONCE: once its row is "imported", it's skipped
  forever, regardless of later content changes (so a rollback->edit->upgrade
  cycle won't re-import).

Shared once-ever gate in import/gate.ts used by both the server (Phase 1) and
desktop importers. migration_state schema: replace backup_path with path + hash;
migrations squashed to a single unreleased baseline.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant