Skip to content

feat(desktop): load runtime self-host config#2012

Open
congvc-dev wants to merge 3 commits intomultica-ai:mainfrom
congvc-dev:feat/desktop-runtime-config
Open

feat(desktop): load runtime self-host config#2012
congvc-dev wants to merge 3 commits intomultica-ai:mainfrom
congvc-dev:feat/desktop-runtime-config

Conversation

@congvc-dev
Copy link
Copy Markdown

@congvc-dev congvc-dev commented May 2, 2026

What does this PR do?

Adds a runtime self-host configuration path for the Electron desktop app so packaged Desktop builds can point at a self-hosted Multica instance without rebuilding the app or hand-editing VITE_* build-time env.

The source of truth is the code changelist: packaged Desktop now reads ~/.multica/desktop.json before renderer startup. If the file is missing, Desktop keeps the Cloud defaults. If the file exists but is invalid, Desktop fails closed with a blocking config error instead of silently falling back to Cloud.

Related Issue

Closes #1371
Refs #1768
Refs #1777

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Refactor / code improvement (no behavior change)
  • Documentation update
  • Tests (adding or improving test coverage)
  • CI / infrastructure

Changes Made

  • apps/desktop/src/shared/runtime-config.ts
    • Adds schema-v1 runtime config parsing for ~/.multica/desktop.json.
    • Requires schemaVersion: 1 and apiUrl.
    • Allows optional wsUrl and appUrl; derives them from apiUrl when omitted.
    • Uses Cloud defaults when no config file exists.
    • Rejects malformed JSON, unsupported schema versions, empty fields, invalid URL schemes, and invalid URL values.
  • apps/desktop/src/main/runtime-config-loader.ts
    • Loads runtime config from the user's Multica config directory before renderer startup.
    • Preserves electron-vite dev env precedence for VITE_API_URL / VITE_WS_URL / VITE_APP_URL.
    • Fails closed when a present config file is invalid instead of silently falling back to Cloud.
  • apps/desktop/src/main/index.ts, apps/desktop/src/preload/index.ts, apps/desktop/src/preload/index.d.ts
    • Exposes the validated runtime config or config error through preload/IPC.
  • apps/desktop/src/renderer/src/App.tsx, apps/desktop/src/renderer/src/pages/login.tsx, apps/desktop/src/renderer/src/platform/navigation.tsx
    • Uses resolved runtime apiUrl, wsUrl, and appUrl before the first API/client request.
    • Shows a blocking config error screen with path + sample JSON when desktop.json is invalid.
    • Passes the resolved runtime API URL into daemon target setup before auto-start/token sync.
  • apps/desktop/src/shared/runtime-config.test.ts, apps/desktop/src/main/runtime-config-loader.test.ts
    • Adds parser/default/validation and loader behavior coverage.
  • apps/docs/content/docs/desktop-app.mdx, apps/docs/content/docs/desktop-app.zh.mdx
    • Documents the new ~/.multica/desktop.json self-host path, required/optional fields, derived URL behavior, invalid-config failure mode, and dev-env precedence.
  • apps/docs/content/docs/self-host-quickstart.mdx, apps/docs/content/docs/self-host-quickstart.zh.mdx
    • Updates self-host next steps to point to Desktop runtime config while keeping web frontend + CLI as the quickest path.

Runtime config format

Minimal self-host config:

{
  "schemaVersion": 1,
  "apiUrl": "https://api.your-domain"
}

Optional explicit URL config:

{
  "schemaVersion": 1,
  "apiUrl": "https://api.your-domain",
  "wsUrl": "wss://api.your-domain/ws",
  "appUrl": "https://your-domain"
}

apiUrl must use http or https; wsUrl must use ws or wss. Search params and hashes are stripped during normalization, and trailing slashes are trimmed.

How to Test

  1. Run desktop checks:
    pnpm --filter @multica/desktop typecheck
    pnpm --filter @multica/desktop test
  2. Run docs validation:
    pnpm --filter @multica/docs typecheck
  3. Self-host smoke with a config file:
    mkdir -p ~/.multica
    cat > ~/.multica/desktop.json <<'JSON'
    {
      "schemaVersion": 1,
      "apiUrl": "https://congvc-x99.taila6fa8a.ts.net:18443"
    }
    JSON
    pnpm --filter @multica/desktop dev
    Confirm Desktop uses:
    • API: https://congvc-x99.taila6fa8a.ts.net:18443
    • WS: wss://congvc-x99.taila6fa8a.ts.net:18443/ws
    • App URL: https://congvc-x99.taila6fa8a.ts.net:18443
  4. Corrupt ~/.multica/desktop.json and relaunch Desktop. Confirm it shows a blocking runtime config error instead of Cloud login.
  5. Remove ~/.multica/desktop.json and relaunch Desktop. Confirm existing Cloud/default behavior still works.
  6. For macOS artifact verification, build on macOS:
    pnpm install --frozen-lockfile
    pnpm --filter @multica/desktop package -- --mac --arm64 --publish never
    pnpm --filter @multica/desktop package -- --mac --x64 --publish never

Local verification run:

  • pnpm --filter @multica/desktop typecheck
  • pnpm --filter @multica/desktop test ✅ — 8 files / 89 tests
  • pnpm --filter @multica/docs typecheck

Linux note: release-quality .dmg/.zip packaging still requires a macOS runner for Electron mac targets/signing/notarization.

Checklist

  • I have included a thinking path that traces from project context to this change
  • I have run tests locally and they pass
  • I have added or updated tests where applicable
  • If this change affects the UI, I have included before/after screenshots
  • I have updated relevant documentation to reflect my changes
  • I have considered and documented any risks above
  • I will address all reviewer comments before requesting merge

AI Disclosure

AI tool used: Multica Agent / Hermes Driver Plane, with Codex implementation and documentation handoff in the Multica workspace.

Prompt / approach:
Implemented from the accepted Multica workspace design thread and then corrected against the actual PR #2012 code changelist as source truth: define ~/.multica/desktop.json, parse/validate in the main process, expose through preload before renderer startup, replace baked Desktop URLs with resolved runtime config, document the runtime config path, and verify with targeted desktop/docs checks.

Screenshots (optional)

Not included. UI change is limited to a blocking runtime config error screen for invalid local config; primary behavior is startup/runtime configuration wiring.

Co-authored-by: multica-agent <github@multica.ai>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 2, 2026

Someone is attempting to deploy a commit to the IndexLabs Team on Vercel.

A member of the Team first needs to authorize it.

@congvc-dev congvc-dev marked this pull request as draft May 2, 2026 17:25
Co-authored-by: multica-agent <github@multica.ai>
@congvc-dev congvc-dev marked this pull request as ready for review May 2, 2026 22:19
Copy link
Copy Markdown
Collaborator

@Bohan-J Bohan-J left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR — the design and layering look solid (clean shared/main split, fail-closed semantics, schema-versioned JSON, sync-IPC-at-preload-time matches appInfo). Before merge, please address the must-fix below and the first three nits.

Must-fix

1. Add a prod-mode happy-path test for loadRuntimeConfig.

apps/desktop/src/main/runtime-config-loader.test.ts currently covers:

  • dev + desktop.json present (env wins)
  • prod + missing file (cloud defaults)
  • prod + invalid JSON (fail closed)

…but not the core path of this feature: prod + a valid desktop.json → returns the parsed config. parseRuntimeConfig is unit-tested in isolation, but the loader's readFile + parse plumbing isn't directly exercised. Suggested:

it("parses a valid packaged desktop.json", async () => {
  const dir = await mkdtemp(join(tmpdir(), "multica-desktop-config-"));
  const configPath = join(dir, "desktop.json");
  await writeFile(
    configPath,
    JSON.stringify({ schemaVersion: 1, apiUrl: "https://api.example.com" }),
  );

  await expect(
    loadRuntimeConfig({ isDev: false, configPath, env: {} }),
  ).resolves.toEqual({
    ok: true,
    config: {
      schemaVersion: 1,
      apiUrl: "https://api.example.com",
      wsUrl: "wss://api.example.com/ws",
      appUrl: "https://api.example.com",
    },
  });
});

Nits to address

2. The APP_URL = "" / WEB_URL = "" fallbacks are confusing dead code.

In apps/desktop/src/renderer/src/platform/navigation.tsx:19-21 and apps/desktop/src/renderer/src/pages/login.tsx:5-7:

const APP_URL = window.desktopAPI.runtimeConfig.ok
  ? window.desktopAPI.runtimeConfig.config.appUrl
  : "";

App.tsx:281-292 already short-circuits the entire CoreProvider subtree with <BlockingRuntimeConfigError /> when runtimeConfig.ok === false, so neither navigation.tsx nor login.tsx can ever render with the empty-string fallback active. The : "" branch is unreachable but reads as a legitimate fallback to a future maintainer. Please either:

  • Throw with an "invariant violated" message in the else branch, or
  • Compute these as runtimeConfig.config.appUrl directly with a non-null assertion + comment that explains the upstream guard.

Either makes the invariant explicit; the current shape silently encodes "empty APP_URL is an OK runtime state," which it isn't.

3. apps/desktop/.env.production is now orphaned.

The renderer no longer reads import.meta.env.VITE_API_URL / VITE_WS_URL / VITE_APP_URL for these URLs, and the values in .env.production happen to match DEFAULT_RUNTIME_CONFIG exactly, so it's not a behavioral bug — but the file is dead code with a misleading header comment ("electron-vite ... reads this automatically in production mode and inlines the values into the renderer bundle"). The next person who wants to change a Cloud URL will edit this file and wonder why nothing changes. Please either delete the file or rewrite the header to say "kept as historical reference; runtime config now flows through ~/.multica/desktop.json and DEFAULT_RUNTIME_CONFIG in apps/desktop/src/shared/runtime-config.ts."

4. Verify process.env.VITE_* actually reaches main in dev mode.

apps/desktop/src/main/index.ts:198-204 reads dev URLs from process.env.VITE_API_URL / VITE_WS_URL / VITE_APP_URL, but the previous code path used import.meta.env.VITE_* (Vite build-time substitution) in the renderer. These are different sources:

  • import.meta.env.VITE_* is filled in by Vite from .env.development at build time.
  • process.env.VITE_* only contains values that are actually set in the spawned process's environment.

Whether electron-vite propagates .env.development values into the main process's process.env (vs. only into import.meta.env) depends on the toolchain config. If a developer sets VITE_API_URL=http://localhost:8081 in .env.development (without exporting it in their shell), with this PR the main process likely won't see it, fall back to the hard-coded http://localhost:8080, and setTargetApiUrl will hand the daemon a URL that doesn't match the renderer.

Please verify by either:

  • Smoke-testing electron-vite dev with a custom .env.development (VITE_API_URL=http://localhost:8081) and confirming the main process picks it up; or
  • Switching the main-process side to import.meta.env.VITE_API_URL (electron-vite supports import.meta.env in main as well), which keeps parity with the previous build-time path.

If smoke test passes, please add a one-line note in the loader explaining the assumption (e.g., "electron-vite injects VITE_* from .env.[mode] into process.env for the main process") so future-you doesn't re-question this.


Remaining items from my full review (Windows path string in the blocking error screen, deriveAppUrl reusing the API origin) are informational only — happy to leave those for a follow-up.

Co-authored-by: multica-agent <github@multica.ai>
@congvc-dev congvc-dev requested a review from Bohan-J May 4, 2026 10:45
Copy link
Copy Markdown
Collaborator

@Bohan-J Bohan-J left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed b48b167 — all four asks addressed cleanly. CI green (backend + frontend). Approving.

1. Prod happy-path test — done. runtime-config-loader.test.ts now exercises the readFile + parseRuntimeConfig chain end-to-end. Matches the suggestion verbatim.

2. APP_URL fallback — done. Replaced the : "" dead branch with requireRuntimeAppUrl(scope) that throws an "Invariant violated" error if called when runtimeConfig.ok === false. Three callsites (DesktopNavigationProvider, TabNavigationProvider, DesktopLoginPage) all guarded; appUrl correctly added to the useMemo deps in both navigation providers. The invariant is upheld by App.tsx's runtimeConfigResult.ok ? gate, so the throw is unreachable in practice but loudly explicit if it ever isn't.

3. .env.production — deleted. No more orphaned file; Cloud defaults live solely in DEFAULT_RUNTIME_CONFIG.

4. Dev env source — switched to import.meta.env. apps/desktop/src/main/index.ts:196-213 now reads import.meta.env.VITE_* (electron-vite's build-time substitution path) instead of process.env.VITE_*, with a comment explaining why. This keeps parity with the renderer's previous behavior and removes the propagation-via-process.env uncertainty.

Nothing else to flag — design, layering, fail-closed semantics, and IPC ordering were already solid. Ready to merge.

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.

[Feature]: Support self-hosted base URL discovery/configuration in the desktop app

2 participants