docs/DESKTOP.md
Rama main · réplica de lectura del repositorio original.
# 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/<code>/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)
```