# LobbyForge Desktop The official native client for LobbyForge. Built with **Tauri 2** — a thin, capability-scoped Rust shell that wraps a WebView2/WebKitGTK/WKWebView window around a self-hosted LobbyForge instance. ## Connect-to-server model The desktop app does **not** bundle the web app. LobbyForge's web app is a Next.js server (SSR + API routes + Postgres + Redis + LiveKit) that runs on your own server. The desktop client is a native window that loads your instance URL inside a sandboxed webview and adds native capabilities the browser can't provide. **One official installer serves every instance.** On first launch the client asks for the instance URL and remembers it. There is no per-instance branding or build step — the same `.msi` / `.dmg` / `.deb` works for everyone. ``` ┌──────────────────────────┐ ┌─────────────────────────────┐ │ Desktop client (Tauri) │ │ Your LobbyForge instance │ │ │ │ (Next.js + PG + Redis + │ │ ┌────────────────────┐ │ HTTPS │ LiveKit on your server) │ │ │ WebView →─────────┼──┼────────►│ │ │ │ instance URL │ │ │ /lobby /admin /api/... │ │ └────────────────────┘ │ │ │ │ + global PTT shortcut │ └─────────────────────────────┘ │ + tray + notifications │ │ + single instance │ └──────────────────────────┘ ``` ## Download Pre-built installers are attached to every LobbyForge **GitHub Release** (`v*` tags), next to the server image, with SHA-256 checksums. They are built by `desktop-release.yml`, which `release.yml` calls: - **Windows**: `desktop-windows-LobbyForge_x.y.z_x64-setup.exe`. One click, no admin prompt: installs for the current user in `%LOCALAPPDATA%\LobbyForge` and opens the app. An `.msi` (per machine, admin) is added for versions without a pre-release suffix. - **macOS**: `.dmg` - **Linux**: `.deb`, `.rpm` or `.AppImage` Code signing turns on when the signing secrets are configured; until then Windows SmartScreen and macOS Gatekeeper warn on first run. See [DESKTOP_SIGNING.md](DESKTOP_SIGNING.md) for the installer's switches and the signing setup. ## First launch 1. Install the client. 2. On the connect screen, enter your instance URL (e.g. `https://lobby.example.com`). HTTPS is required; loopback `http://localhost` is permitted only in debug builds for local development. 3. Click **Connect**. The instance loads in the window and the URL is saved for next launch. 4. To switch instances later, open the tray menu and choose **Switch instance…**. It forgets the saved address and brings back the connect screen from any page, including an instance that no longer loads (the app cannot tell a failed load from a slow one, so it does not fall back on its own). The connect screen, its error messages and the tray menu follow the system's language when the instance ships it (`apps/web/messages//desktop.json`), otherwise English. The Windows installer offers the same languages, as far as Tauri's installer translations go. Adding a language needs no code; see [TRANSLATING.md](TRANSLATING.md#the-desktop-app). ## Native capabilities | Capability | Behavior | |------------|----------| | **Global push-to-talk** (opt-in) | Off by default. Tick **Push to talk anywhere (Ctrl+Space)** in the tray menu, then hold `Ctrl+Space` anywhere (even when the client isn't focused) to talk; the client forwards the key state to the instance via a `lobbyforge:ptt` event. The choice is saved (`globalPushToTalk` in the app's `settings.json`). A global shortcut takes the key away from every other app — `Ctrl+Space` switches the input language on many systems — so it is registered only while an instance is shown and released on disconnect. Push-to-talk inside the focused window works without it (the instance's own key binds). | | **Other global shortcuts** (off) | `Ctrl+Shift+M` (mute), `Ctrl+Shift+D` (deafen) and `Ctrl+,` (settings) are registered system-wide only when `globalShortcuts` is `true` in `settings.json`; there is no menu item for them. | | **Tray icon** | Hide the window to the system tray; menu: Show / Switch instance… / Push to talk anywhere / Quit. | | **Single instance** | A second launch focuses the existing window instead of opening a duplicate. | | **Persisted instance** | The last-connected URL is restored automatically on launch; **Switch instance…** in the tray forgets it. | | **Deep-link auth** (infrastructure) | `lobbyforge://session/complete?code=&state=&instance=` handler for the OAuth session handoff flow. | ## Build from source ### Prerequisites - **Rust** (stable, MSVC toolchain on Windows): `rustup default stable-x86_64-pc-windows-msvc` - **Windows**: Microsoft C++ Build Tools (MSVC) + Windows 11 SDK (via Visual Studio Installer → "Desktop development with C++") - **Linux**: `libwebkit2gtk-4.1-dev librsvg2-dev patchelf` - **macOS**: Xcode Command Line Tools - **Node.js ≥ 22**, **pnpm ≥ 10.12.1** ### Develop ```sh pnpm install pnpm --filter @lobbyforge/desktop dev # opens the window (tauri dev) ``` ### Produce an installer ```sh pnpm --filter @lobbyforge/desktop build # tsc + tauri build # → apps/desktop/src-tauri/target/release/bundle/ ``` ### Publish a release Desktop installers ship with the unified release: pushing a `v*` tag runs `release.yml`, which calls `desktop-release.yml` to build (and, when configured, sign) all three platforms with the tag's version, then attaches them to the GitHub Release. `desktop-v*` tags are no longer used. For a test build, run **Actions → desktop-release → Run workflow**: the installers are attached to the run, and a Windows smoke test installs, launches, reinstalls and uninstalls the NSIS build. ## Security - The instance webview is **isolated per origin** — it gets no shell, filesystem, process, or updater capability. Only the audited desktop event bridge (PTT forwarding) crosses the boundary. - Instance URL validation enforces HTTPS origins only (loopback http in debug builds). Credentials, query strings, and fragments are rejected. - Deep-link auth codes are never logged — the shell redacts `code=` before writing to logs (see `redactDesktopHandoff` in `src/index.ts`). - **Navigation is pinned** (security-review DESK-V-01). The window has no address bar, so it only ever shows the connected instance (its exact origin) and the bundled connect screen. Any other `http(s)` page — a link, a redirect, a script navigating the window — opens in the system browser instead (at most one a second); other schemes (`file:`, `data:`, custom protocols) are dropped; `window.open` / `target=_blank` never creates a second app window. The decision is `decide_top_level_navigation` in `src-tauri/src/lib.rs`. On macOS and Linux the webview asks about iframe navigations too, so embeds the instance frames on purpose (Watch Party's YouTube player, a CAPTCHA widget) load, and a top-level page outside the instance is caught on page load and replaced by the instance. One consequence: sign-in pages of other sites (e.g. "Continue with Google") open in the system browser — sign in with the instance's own form in the app. - **The browser-login handoff goes to its instance only** (DESK-V-02). The deep link's code + state are posted to the origin the login was started for, never broadcast (`'*'`), and connecting to another instance or disconnecting drops a login that is still pending. ## Layout ``` apps/desktop/ src/ # TS contracts (handoff validators, shortcut maps) + tests src-tauri/ # Rust native shell (Tauri 2) Cargo.toml tauri.conf.json src/{lib.rs, main.rs} capabilities/ # permission scopes icons/ # generated icon set (pnpm tauri icon) dist-shell/ # connect-screen frontend (static HTML/JS) ```