* feat(den-api): expose desktop config from env * feat(desktop): persist den bootstrap config across updates * feat(den): manage desktop restrictions per organization * fix(app): stabilize cloud org selection * docs(desktop): add bootstrap config PRD --------- Co-authored-by: src-opn <src-opn@users.noreply.github.com>
11 KiB
OpenWork Desktop Bootstrap And Org Runtime Config
Goal
Support three desktop distribution modes while still allowing all of them to update from the default app build:
- Default build pointing at the default OpenWork server with no forced sign-in.
- Custom build pointing at the default OpenWork server with forced sign-in.
- Custom build pointing at a client-specific server with forced sign-in.
After install, the desktop app should preserve its initial server/sign-in behavior across app updates, then fetch org-specific runtime config from the server after the user signs in.
Non-Goals
- No server-wide default desktop config.
- No anonymous org config fetch before sign-in.
- No attempt to make the web build own persisted bootstrap state.
Product Decision
Use two config layers only:
bootstrap configorg runtime config
Bootstrap config
Bootstrap config is install-level configuration. It is set by the app build, then persisted locally by the desktop app so it survives future updates.
It contains only:
server.baseUrlserver.apiBaseUrlif neededrequireSignin
Bootstrap config decides startup behavior before normal app UI renders.
Current apps/app locations
Today, the main Den bootstrap URL logic in apps/app lives in:
_repos/openwork/_worktrees/Desktop-config-den-endpoint/apps/app/src/app/lib/den.ts
Specifically:
DEFAULT_DEN_BASE_URLis sourced fromimport.meta.env.VITE_DEN_BASE_URL- the stored Den API URL is tracked as
apiBaseUrl resolveDenBaseUrls(...)derives or resolves both the user-facing Den base URL and the API base URLreadDenSettings()andwriteDenSettings()are the current read/write path for persisted Den URL state
This PRD assumes that the future bootstrap-config implementation will either extend this location or replace it with a more durable desktop-owned persisted config source, while keeping apps/app as the consumer of that state.
Ownership model
For the update-safe version of this system, apps/app should not be the source of truth for bootstrap config.
Instead:
- the desktop shell owns persisted bootstrap config
apps/appconsumes resolved bootstrap config- build-time env values are only used to seed first launch
This means apps/app should stop treating VITE_DEN_BASE_URL or browser-side persisted URL state as authoritative in desktop mode after bootstrap config has been created.
How config should be passed into apps/app
Recommended flow:
- The desktop shell starts first.
- It loads persisted bootstrap config from desktop app data.
- If no persisted bootstrap config exists yet, it seeds one from build-time defaults.
- The desktop shell passes the resolved bootstrap config into
apps/appbefore normal UI startup completes. apps/appuses that resolved config for Den base URL, API base URL, andrequireSignin.
Recommended interface:
getBootstrapConfig()setBootstrapConfig()if editing is allowed
This can be implemented with a Tauri command, a shell-injected bootstrap object, or another desktop-owned bridge. The important part is that the desktop shell is the owner and apps/app is the consumer.
Build-time environment variables
The desktop app supports build-time bootstrap seeding for custom distributions.
Preferred desktop build variables:
OPENWORK_DESKTOP_DEN_BASE_URLOPENWORK_DESKTOP_DEN_API_BASE_URLOPENWORK_DESKTOP_DEN_REQUIRE_SIGNIN
Fallback variables currently recognized by the app and desktop build:
VITE_DEN_BASE_URLVITE_DEN_API_BASE_URLVITE_DEN_REQUIRE_SIGNIN
These values are baked into the desktop build as the initial bootstrap seed. After launch, the desktop shell resolves the effective bootstrap config using the external bootstrap file and any persisted bootstrap state.
Example
OPENWORK_DESKTOP_DEN_BASE_URL="https://client.example.com" \
OPENWORK_DESKTOP_DEN_API_BASE_URL="https://client.example.com/api/den" \
OPENWORK_DESKTOP_DEN_REQUIRE_SIGNIN="1" \
pnpm --filter @openwork/desktop build
Accepted boolean values
OPENWORK_DESKTOP_DEN_REQUIRE_SIGNIN and VITE_DEN_REQUIRE_SIGNIN are treated as enabled when set to one of:
1trueyeson
Update-safe bootstrap behavior
To preserve custom builds across updates:
- A one-time custom build ships with seed values such as server URL and
requireSignin. - On first launch, the desktop shell copies those seed values into persisted desktop app data.
- Future app updates may install the default build artifacts.
- On next launch, the desktop shell reads the previously persisted bootstrap config instead of reusing the default build values.
apps/appreceives the persisted bootstrap config and starts with the correct URLs and sign-in behavior.
This ensures that updates do not silently reset a client build back to the default server or disable forced sign-in.
Storage boundary
Bootstrap config should live outside the bundled web app and outside browser-only storage.
Recommended location:
- Tauri-side persisted store or app-data file
For the current external bootstrap-file approach, the desktop shell should look for desktop-bootstrap.json in the host config directory under OpenWork/, unless an explicit override path is provided with OPENWORK_DESKTOP_BOOTSTRAP_PATH.
Expected default locations:
- macOS:
~/Library/Application Support/OpenWork/desktop-bootstrap.json - Linux:
~/.config/OpenWork/desktop-bootstrap.json - Windows:
%AppData%\OpenWork\desktop-bootstrap.json
Avoid using browser localStorage as the long-term source of truth for install identity in desktop mode.
localStorage may still be used as a compatibility layer or temporary cache inside apps/app, but the authoritative desktop bootstrap config should live in shell-owned persisted storage.
Desktop startup contract
In desktop mode, startup should behave like this:
- shell loads bootstrap config
- shell returns bootstrap config to
apps/app apps/appinitializes URL resolution from shell-provided valuesapps/appappliesrequireSigninbefore rendering normal UI- signed-in flows then fetch org runtime config
In web mode, apps/app can continue using its existing env-based defaults because there is no desktop shell layer.
Org runtime config
Org runtime config is authenticated, org-specific configuration fetched from the server after sign-in.
Initial examples:
disallowNonCloudModelsblockZenModelblockMultipleWorkspaces
Org runtime config should use sparse negative restriction keys. An empty object means the desktop app keeps its normal default behavior.
In the cloud admin UI, these restrictions may be presented as positive capability toggles for clarity. For example, the UI can show Allow non-cloud deployed models, while the stored config saves disallowNonCloudModels: true only when that capability is turned off.
This config is cached locally, applied in memory while the app runs, and refreshed from the server over time.
Why this split exists
Builds 2 and 3 will only be built once, but later update using artifacts from build 1. That means any important startup behavior cannot live only in the bundled web app or binary defaults.
So:
- build-time config seeds the first launch
- the desktop shell persists bootstrap config locally
- later updates read that persisted bootstrap config instead of replacing it
This preserves custom server targeting and forced sign-in across updates.
Required behavior
Startup
On desktop app start:
- Load persisted bootstrap config.
- Use it to resolve the Den server / API base.
- If
requireSigninis true and there is no valid signed-in session, show a blocking full-screen sign-in screen before rendering normal app UI. - If the user is signed in, load cached org runtime config immediately.
- Fetch fresh org runtime config from the server.
- Apply fresh config in memory when it arrives.
Refresh triggers for org runtime config
The app should download org runtime config:
- on every successful sign-in
- on every app start when the user is already signed in
- every 1 hour while the app is running
Update behavior
App updates must not overwrite persisted bootstrap config.
The app should continue using the locally persisted bootstrap config after updating, even if the new binary is the default build.
Suggested storage model
Persisted desktop storage
Store these values in desktop app data, not just in browser localStorage:
- bootstrap config
- last known org runtime config
- metadata for the cached org config if needed, such as fetched time or org id
In-memory runtime state
During app execution, maintain the current effective config in memory.
Recommended merge order:
- bootstrap config
- cached org runtime config
- freshly fetched org runtime config
Bootstrap config should only contain startup-critical fields. Org runtime config should control ongoing product behavior.
Server contract
Server config is org-specific and authenticated.
Recommended endpoint:
GET /v1/me/desktop-config
Example response:
{
"blockZenModel": true,
"blockMultipleWorkspaces": true
}
requireSignin should not be part of this runtime payload. It belongs in bootstrap config.
Distribution modes
1. Default build
server.baseUrl: default OpenWork serverrequireSignin:false
2. Custom build against default server
server.baseUrl: default OpenWork serverrequireSignin:true
3. Custom build against client server
server.baseUrl: client-specific serverrequireSignin:true
For modes 2 and 3, the build is only used to seed first launch. After that, the persisted bootstrap config is the source of truth.
UX requirements
- Forced sign-in must block normal app UI until a valid session exists.
- Manual navigation to other routes while forced sign-in is active should redirect back to the sign-in screen.
- Once signed in, the app can load and apply org runtime config.
- If cached org runtime config exists, the app may use it immediately while refreshing in the background.
Open implementation notes
- Prefer a Tauri-side persisted store or app-data file for bootstrap config instead of relying on web storage alone.
- The desktop shell should own persisted bootstrap state; the web UI should consume it.
- If org changes matter for config, the cache key should include org identity.
- If the org runtime config fetch fails, keep using the last known cached config when available.
Success criteria
- A custom build can point at a custom server, update from the default release artifacts, and still keep its server target and forced sign-in behavior.
- Forced sign-in works before normal UI renders.
- Org runtime config is refreshed on sign-in, startup, and hourly.
- Model-related config can change over time without rebuilding the app.