--- title: "Recognize signed-in customers in a desktop app | Busymate AI" description: "Electron, Tauri or WebView2: mint before you navigate, put the proof in the URL fragment, then signal sign-in, sign-out and resume through the preload." last_updated: "2026-09-21T07:42:22+03:00" --- # Recognize signed-in customers in a desktop app | Busymate AI Source: https://busymate.ai/es/docs/guides/identity-desktop Last modified: 2026-09-21T07:42:22+03:00 Busymate AI treats your desktop shell as its own platform. An Electron, Tauri or WebView2 window loads the assistant as the **top-level** document — there is no parent window to ask, so first launch carries identity differently than a website or a mobile WebView does, and everything after first launch goes through a small preload instead. ## 1. Mint before you navigate, not after With no parent to ask, first launch is served well by asking nobody: your main process mints the sign-in proof itself and puts the pair in the URL **fragment** before the window ever loads the address. ```typescript // Full-page open with identity in the URL FRAGMENT — never sent in the // request line, referrer, or logs; the destination strips it before exchange. function newLaunchNonce() { const bytes = new Uint8Array(32); crypto.getRandomValues(bytes); return btoa(String.fromCharCode(...bytes)) .replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, ""); } const nonce = newLaunchNonce(); const accessToken = await getProductAccessToken(); // YOUR existing auth helper if (!accessToken) throw new Error("Sign in before opening an identified assistant"); const returnTo = new URL(location.href); returnTo.search = ""; returnTo.hash = ""; const response = await fetch("https://YOUR-PRODUCT-DOMAIN/api/bmai/identity", { method: "POST", credentials: "include", cache: "no-store", headers: { "content-type": "application/json", authorization: "Bearer " + accessToken }, body: JSON.stringify({ nonce, returnTo: returnTo.href }), }); if (response.status === 401) throw new Error("Sign in before opening an identified assistant"); if (!response.ok) throw new Error("AI identity mint failed (" + response.status + ")"); const identity = await response.json(); if (typeof identity.token !== "string" || typeof identity.nonce !== "string") { throw new Error("AI identity mint returned an invalid response"); } if (identity.nonce !== nonce) throw new Error("AI identity nonce mismatch"); const url = new URL("https://your-assistant.busymate.ai/"); url.hash = new URLSearchParams({ bmai_token: identity.token, bmai_nonce: identity.nonce, }).toString(); location.assign(url); ``` `channel=desktop` (set on the URL, shown above as part of the address) labels the session "Desktop app" everywhere it appears — the Console, the transcript, the checker. ## 2. Copy the preload Electron: `https://busymate.ai/sdk/v1/kit/desktop/preload.js`, with `contextIsolation: true` and `nodeIntegration: false`. Your main process answers the `busymate:mint-identity` IPC channel the preload sends; a runnable main process is at `https://busymate.ai/sdk/v1/kit/desktop/sample-main.js`. - **Tauri** — the same three calls become one `#[tauri::command] fn busymate_mint()` plus `invoke("busymate_mint")` from an init script. Tauri's own `__TAURI_INTERNALS__` global is already recognized as a shell. - **WebView2** — `AddScriptToExecuteOnDocumentCreatedAsync` installs the identical shim; answer over `CoreWebView2.PostWebMessageAsJson`, which delivers a real object rather than a string. ## 3. Everything after first launch goes through the preload No more handshakes: call `busymateDesktop.identityChanged()` on login, token rotation or account switch; `busymateDesktop.signedOut()` on logout; `busymateDesktop.onResume()` when the window regains focus or wakes from sleep. Each mints fresh through the same main-process endpoint as first launch — never a cached pair. ## 4. What you never have to configure Cookies, third-party or otherwise. Identity arrives from your own main process on first launch and on every later call, so nothing in this integration depends on a cookie policy, a partition setting, or your window's session store. ## Verify 1. Launch signed in: the greeting names the right customer on the very first screen, with no visible handshake. 2. Sign out inside your app; the next customer to use the window gets a guest session, not the last one's history. 3. Minimize, wait past the token's lifetime, and restore the window: the assistant is still identified, not silently downgraded to a guest. 4. Run `node v2/scripts/identity-conformance.mjs --host --json` with `channel=desktop`; it names any obligation your shell still misses instead of a bare pass or fail. ### Why mint before the window navigates instead of asking after it loads? A desktop window has no parent to ask, and the resume budget for an in-page ask is short. Minting in the main process and handing over the pair in the URL fragment needs no round trip inside the loaded page at all. ### Does Tauri need the exact same preload file as Electron? No. The three calls collapse to one Rust command your init script invokes; the file it replaces is `preload.js`, not a Tauri equivalent of it. ### Why does WebView2 answer differently from Electron? `CoreWebView2.PostWebMessageAsJson` hands the page a real object. Electron's preload does the equivalent through `contextBridge`, so both avoid re-parsing a string on the page side. ### Is `channel=desktop` required, or only cosmetic? Set it. It labels every session "Desktop app" in the Console and the transcript, and the checker reads it to grade the right lifecycle. ## Next - **[Recognize signed-in customers](https://busymate.ai/es/docs/guides/identified-visitors)** — the contract this shell implements, the claims your mint endpoint signs. - **[Sign identity tokens from your backend](https://busymate.ai/es/docs/guides/identity-backend-signing)** — the endpoint your main process calls. - **[Fix a signed-in customer who shows as a guest](https://busymate.ai/es/docs/guides/identity-troubleshooting)** — symptom-first fixes when a cell fails.