Guides
Recognize signed-in customers in a desktop app
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.
On this page
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.
// 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()plusinvoke("busymate_mint")from an init script. Tauri's own__TAURI_INTERNALS__global is already recognized as a shell. - WebView2 —
AddScriptToExecuteOnDocumentCreatedAsyncinstalls the identical shim; answer overCoreWebView2.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
- Launch signed in: the greeting names the right customer on the very first screen, with no visible handshake.
- Sign out inside your app; the next customer to use the window gets a guest session, not the last one's history.
- Minimize, wait past the token's lifetime, and restore the window: the assistant is still identified, not silently downgraded to a guest.
- Run
node v2/scripts/identity-conformance.mjs --host <your-host> --jsonwithchannel=desktop; it names any obligation your shell still misses instead of a bare pass or fail.
Next
- Recognize signed-in customers — the contract this shell implements, the claims your mint endpoint signs.
- Sign identity tokens from your backend — the endpoint your main process calls.
- Fix a signed-in customer who shows as a guest — symptom-first fixes when a cell fails.
Questions
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.PostWebMessageAsJsonhands the page a real object. Electron's preload does the equivalent throughcontextBridge, 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.