# Phase 6: Desktop Client & CI/CD - Research
**Researched:** 2026-06-25
**Domain:** Tauri 2.x Desktop Wrapper + Gitea Actions CI/CD
**Confidence:** MEDIUM
## Summary
Phase 6 consists of two independent tracks: (A) a Tauri 2.x desktop wrapper that loads the existing Tessera web frontend via URL, and (B) Gitea-based DevOps tooling with CI/CD pipelines. The desktop app is a thin shell -- no frontend code is bundled, it simply opens a WebView pointing at the configured server URL. All desktop-specific behavior (system tray, notifications, window state, autostart) is handled by official Tauri plugins with minimal custom Rust code.
The Gitea Actions CI/CD track uses GitHub Actions-compatible YAML syntax in `.gitea/workflows/`. An `act_runner` container executes pipeline jobs in Docker. The pipeline runs Lint + TypeCheck, Tests (Vitest), Docker image builds, and auto-deploys via `docker-compose pull && restart` on the same server. This is purely developer tooling -- no Git/Gitea features are exposed in the Tessera application.
A critical prerequisite: **Rust toolchain and WebKitGTK development headers are NOT installed** on the current machine. These must be installed before any Tauri development or builds can proceed. The system has GTK3, librsvg2, and libayatana-appindicator3 (runtime libs), but the **-dev packages for WebKitGTK and the Rust compiler are missing**.
**Primary recommendation:** Install Rust via rustup and WebKitGTK dev headers first, then scaffold `apps/desktop` with `npx tauri init`, configure `frontendDist` to the server URL, and wire up four official Tauri plugins (store, notification, autostart, window-state). For CI/CD, add `act_runner` to docker-compose and create workflow files in `.gitea/workflows/`.
## User Constraints (from CONTEXT.md)
### Locked Decisions
- **D-01:** Wrapper mit System Tray und nativen OS-Benachrichtigungen (z.B. Kalender-Erinnerungen). Braucht Backend-Integration fuer Notification-Events.
- **D-02:** Server-URL konfigurierbar beim ersten Start. Wird lokal gespeichert. Ein Build fuer alle Umgebungen.
- **D-03:** Beim Schliessen des Fensters (X-Button) wird in System Tray minimiert. Beenden nur ueber Tray-Kontextmenu.
- **D-04:** Autostart-Option in Desktop-App-Einstellungen vorhanden, standardmaessig deaktiviert.
- **D-05:** Fensterposition und -groesse werden beim Schliessen gespeichert, beim naechsten Start wiederhergestellt. Erster Start: 1280x800 zentriert.
- **D-06:** Eigenes Tessera-App-Icon (basierend auf Design-System, OKLCH Farben).
- **D-07:** Linux: AppImage als primaeres Format (distro-uebergreifend).
- **D-08:** Update-Hinweis: App prueft beim Start ob neue Version verfuegbar, zeigt Notification. Download bleibt manuell. Auto-Update kann spaeter nachgeruestet werden.
- **D-09:** Code Signing kommt spaeter -- erst relevant wenn Tessera an externe Kunden verkauft wird. Fuer interne Nutzung ohne Signierung OK.
- **D-10:** Gitea ist reines Versionskontroll- und CI/CD-Tooling. Tessera hat keine Git/Gitea-Funktionalitaet in der App.
- **D-11:** Claude pusht manuell bei Meilensteinen oder grossen Bugfixes nach Gitea. Kein automatischer Sync, kein zeitgesteuerter Push.
- **D-12:** Gitea Actions CI/CD Pipeline wird bei jedem Push getriggert: Lint + TypeCheck -> Tests (Vitest) -> Docker Images bauen -> Auto-Deploy.
- **D-13:** Deploy-Ziel ist gleicher Server wie Gitea. Pipeline macht docker-compose pull + restart.
### Claude's Discretion
- Titelleiste: Nativ vs. Custom -- basierend auf Aufwand und Design-System
- App-Menueleiste: Kein Menu vs. minimales Menu -- basierend auf Plattform-Konventionen
- Windows Installer-Format: MSI vs. NSIS -- basierend auf Zielgruppe (intern, spaeter extern)
- Gitea Actions Workflow-Struktur und Stage-Konfiguration
- Docker Image Registry: Gitea-intern vs. lokal
### Deferred Ideas (OUT OF SCOPE)
- Auto-Update mit Tauri Updater
- Code Signing
- macOS Support
- Gitea Webhooks fuer externe Events
## Phase Requirements
| ID | Description | Research Support |
|----|-------------|------------------|
| DESK-01 | Tauri-basierter Desktop-Wrapper fuer Windows und Linux | Tauri 2.x with `frontendDist` URL loading, AppImage (Linux) and NSIS (Windows) bundle targets |
| DESK-02 | Desktop-App verbindet sich mit dem Web-Backend (kein eigenstaendiger Server) | `frontendDist` set to configurable external URL, tauri-plugin-store for persisting server URL |
| INFRA-04 | Automatisierte Gitea-Integration (Commits, Pushes, Merges) | Gitea Actions with act_runner in Docker, workflow in `.gitea/workflows/`, auto-deploy via docker-compose |
## Architectural Responsibility Map
| Capability | Primary Tier | Secondary Tier | Rationale |
|------------|-------------|----------------|-----------|
| WebView shell (URL loading) | Desktop Client (Tauri/Rust) | -- | Tauri owns the native window and WebView; web content comes from the server |
| System tray + close-to-tray | Desktop Client (Tauri/Rust) | -- | OS-level integration via TrayIconBuilder, window event interception |
| Native notifications | Desktop Client (Tauri/Rust) | API / Backend | Tauri sends OS notifications; backend must provide notification events (future WebSocket/SSE) |
| Server URL configuration | Desktop Client (Tauri/Rust) | -- | First-run dialog and persistent storage are purely client-side |
| Window state persistence | Desktop Client (Tauri/Rust) | -- | tauri-plugin-window-state handles save/restore automatically |
| Autostart | Desktop Client (Tauri/Rust) | -- | OS-level registration via tauri-plugin-autostart |
| Version check | Desktop Client (Tauri/Rust) | API / Backend | Desktop fetches version endpoint from API; comparison logic in Rust/JS |
| CI/CD pipeline | Infrastructure (Gitea/Docker) | -- | Gitea Actions + act_runner, entirely outside the application |
| Docker image builds | Infrastructure (Gitea/Docker) | -- | Existing Dockerfiles reused by CI pipeline |
| Auto-deploy | Infrastructure (Gitea/Docker) | -- | docker-compose pull + restart on same server |
## Standard Stack
### Core
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| Tauri | 2.x (2.11+) | Desktop shell | 5MB installer vs Electron 150MB, OS-native WebView, Rust backend. Project-locked decision [CITED: CLAUDE.md] |
| @tauri-apps/cli | 2.11.3 | Build tooling | Official Tauri CLI for init, dev, build commands [VERIFIED: npm registry + official docs] |
| @tauri-apps/api | 2.11.1 | JS bridge | Frontend-to-Rust IPC, tray, window management APIs [VERIFIED: npm registry + official docs] |
| Rust toolchain | stable (1.77.2+) | Compilation | Required by Tauri; installed via rustup [CITED: v2.tauri.app/start/prerequisites/] |
### Tauri Plugins
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| @tauri-apps/plugin-store | 2.4.3 | Persistent KV storage | Stores server URL, app preferences. Official plugin [VERIFIED: npm registry + v2.tauri.app/plugin/store/] |
| @tauri-apps/plugin-notification | 2.3.3 | OS notifications | Native desktop notifications for calendar reminders etc. Official plugin [VERIFIED: npm registry + v2.tauri.app/plugin/notification/] |
| @tauri-apps/plugin-autostart | 2.5.1 | System startup | Register/unregister app for OS autostart. Official plugin [VERIFIED: npm registry + v2.tauri.app/plugin/autostart/] |
| @tauri-apps/plugin-window-state | 2.4.1 | Window persistence | Auto-saves/restores window size, position, maximized state. Official plugin [VERIFIED: npm registry + v2.tauri.app/plugin/window-state/] |
### CI/CD Infrastructure
| Component | Version | Purpose | Why Standard |
|-----------|---------|---------|--------------|
| Gitea Actions | (built into Gitea) | CI/CD engine | GitHub Actions-compatible YAML, self-hosted [CITED: docs.gitea.com/usage/actions/act-runner] |
| act_runner | latest | Workflow executor | Official Gitea runner, executes jobs in Docker containers [CITED: docs.gitea.com/usage/actions/act-runner] |
| docker/build-push-action | v6 | Image build | Standard GitHub/Gitea action for Docker builds [ASSUMED] |
| appleboy/ssh-action | v1 | Remote deploy | SSH into deploy target for docker-compose operations [ASSUMED] |
### Alternatives Considered
| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| NSIS (Windows) | MSI (WiX) | MSI only builds on Windows; NSIS supports cross-compilation from Linux. **Recommendation: NSIS** -- enables building Windows installer from the Linux dev machine [CITED: v2.tauri.app/distribute/windows-installer/] |
| Native title bar | Custom title bar | Custom requires more effort and platform testing. **Recommendation: Native** -- simpler, consistent with OS, less Rust code |
| No menu bar | Minimal menu bar | Linux desktop conventions expect a menu. **Recommendation: No menu bar** -- wrapper app has no app-specific actions; tray menu suffices |
| Gitea internal registry | Local Docker images | Internal registry adds complexity. **Recommendation: Local builds** -- act_runner builds images on same server, no push needed. Use `docker compose build` directly |
**Installation (Desktop app):**
```bash
# System prerequisites (Debian/Ubuntu/Trixie)
sudo apt install libwebkit2gtk-4.1-dev build-essential curl wget file \
libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev
# Rust toolchain
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
# Tauri CLI + plugins (from monorepo root)
pnpm add -D @tauri-apps/cli --filter=@tessera/desktop
pnpm add @tauri-apps/api @tauri-apps/plugin-store @tauri-apps/plugin-notification \
@tauri-apps/plugin-autostart @tauri-apps/plugin-window-state --filter=@tessera/desktop
```
## Package Legitimacy Audit
| Package | Registry | Age | Downloads | Source Repo | Verdict | Disposition |
|---------|----------|-----|-----------|-------------|---------|-------------|
| @tauri-apps/cli | npm | 3+ yrs (org) | 1.78M/wk | github.com/tauri-apps/tauri | OK* | Approved -- official Tauri project, SUS flag due to recent version publish only |
| @tauri-apps/api | npm | 3+ yrs (org) | 2.11M/wk | github.com/tauri-apps/tauri | OK* | Approved -- official Tauri project, SUS flag due to recent version publish only |
| @tauri-apps/plugin-store | npm | 2+ yrs | 236K/wk | github.com/tauri-apps/plugins-workspace | OK | Approved |
| @tauri-apps/plugin-notification | npm | 2+ yrs | 293K/wk | github.com/tauri-apps/plugins-workspace | OK | Approved |
| @tauri-apps/plugin-autostart | npm | 2+ yrs | 108K/wk | github.com/tauri-apps/plugins-workspace | OK | Approved |
| @tauri-apps/plugin-window-state | npm | 2+ yrs | 73K/wk | github.com/tauri-apps/plugins-workspace | OK | Approved |
*`@tauri-apps/cli` and `@tauri-apps/api` were flagged SUS by the legitimacy check due to recent publish dates (2026-06-17/19). These are false positives -- both are official packages from the tauri-apps organization with millions of weekly downloads and verified source repos. The "too-new" signal is triggered by the latest version publish, not the package itself.
**Packages removed due to [SLOP] verdict:** none
**Packages flagged as suspicious [SUS]:** @tauri-apps/cli and @tauri-apps/api (false positive -- official packages, override to Approved)
## Architecture Patterns
### System Architecture Diagram
```
Desktop App (Tauri) Server Infrastructure
+---------------------------+ +---------------------------+
| | | |
| Rust Backend | HTTP | Next.js (apps/web) |
| - TrayIconBuilder |--------->| - Server Components |
| - Window event handler | | - App Router |
| - Plugin orchestration | | |
| - Version check logic | +---------------------------+
| | |
| WebView (OS native) | | internal
| - Loads server URL | +---------------------------+
| - No bundled assets | | NestJS (apps/api) |
| | | - REST API |
| Local Storage | | - /health |
| - tauri-plugin-store | | - /version (new) |
| - server-url.json | +---------------------------+
| - preferences.json | |
+---------------------------+ +---------------------------+
| PostgreSQL (db) |
CI/CD Pipeline (Gitea) +---------------------------+
+---------------------------+
| .gitea/workflows/ci.yml |
| 1. Lint + TypeCheck |
| 2. Tests (Vitest) |
| 3. Docker Build |
| 4. Deploy (compose pull) |
+---------------------------+
|
act_runner (Docker)
- Executes jobs
- Docker socket mount
```
### Recommended Project Structure
```
apps/
├── desktop/ # NEW: Tauri desktop wrapper
│ ├── package.json # @tessera/desktop
│ ├── src-tauri/
│ │ ├── Cargo.toml # Rust dependencies + Tauri plugins
│ │ ├── tauri.conf.json # Window config, bundle targets, plugins
│ │ ├── capabilities/
│ │ │ └── default.json # Plugin permissions
│ │ ├── icons/ # App icons (PNG, ICO)
│ │ │ ├── icon.png
│ │ │ ├── icon.ico
│ │ │ └── 128x128.png # Tray icon
│ │ └── src/
│ │ ├── lib.rs # Plugin setup, tray, window events
│ │ └── main.rs # Entry point (generated)
│ └── src/ # Minimal JS for first-run setup page
│ └── setup.html # Server URL configuration form
├── web/ # Existing Next.js app
└── api/ # Existing NestJS API
.gitea/
└── workflows/
└── ci.yml # CI/CD pipeline definition
```
### Pattern 1: URL-Loading WebView Wrapper
**What:** Tauri app that loads an external web URL instead of bundling frontend assets.
**When to use:** When the desktop app is a thin shell around an existing web application.
```json
// Source: v2.tauri.app/reference/config/
// tauri.conf.json
{
"build": {
"frontendDist": "http://localhost:3000",
"devUrl": "http://localhost:3000"
},
"app": {
"withGlobalTauri": true,
"windows": [
{
"label": "main",
"title": "Tessera",
"width": 1280,
"height": 800,
"center": true,
"decorations": true,
"resizable": true
}
]
},
"bundle": {
"active": true,
"targets": ["appimage", "nsis"],
"icon": ["icons/icon.png", "icons/icon.ico"]
}
}
```
**Key insight:** `frontendDist` set to a URL means no assets are embedded. The app must have a configurable URL mechanism -- on first launch, show a setup page (local HTML) where user enters the server URL, then store it via tauri-plugin-store and load the WebView with that URL.
### Pattern 2: Close-to-Tray with System Tray
**What:** Intercept window close, hide to system tray instead of quitting.
**When to use:** Long-running desktop apps that should stay accessible via tray icon.
```rust
// Source: v2.tauri.app/learn/system-tray/ + GitHub discussions
use tauri::{
image::Image,
menu::{MenuBuilder, MenuItem},
tray::TrayIconBuilder,
Manager, WindowEvent, RunEvent,
};
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_store::Builder::new().build())
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_window_state::Builder::default().build())
.plugin(tauri_plugin_autostart::init(
tauri_plugin_autostart::MacosLauncher::LaunchAgent,
None,
))
.setup(|app| {
// Build tray menu
let open = MenuItem::with_id(app, "open", "Oeffnen", true, None::<&str>)?;
let quit = MenuItem::with_id(app, "quit", "Beenden", true, None::<&str>)?;
let menu = MenuBuilder::new(app)
.item(&open)
.separator()
.item(&quit)
.build()?;
// Create tray icon
TrayIconBuilder::new()
.icon(Image::from_bytes(include_bytes!("../icons/icon.png"))?)
.tooltip("Tessera")
.menu(&menu)
.show_menu_on_left_click(false)
.on_menu_event(|app, event| match event.id.as_ref() {
"open" => {
if let Some(w) = app.get_webview_window("main") {
let _ = w.show();
let _ = w.set_focus();
}
}
"quit" => app.exit(0),
_ => {}
})
.on_tray_icon_event(|tray, event| {
if let tauri::tray::TrayIconEvent::Click {
button: tauri::tray::MouseButton::Left,
button_state: tauri::tray::MouseButtonState::Up,
..
} = event
{
let app = tray.app_handle();
if let Some(w) = app.get_webview_window("main") {
let _ = w.show();
let _ = w.set_focus();
}
}
})
.build(app)?;
Ok(())
})
// Intercept window close -> hide to tray
.on_window_event(|window, event| {
if let WindowEvent::CloseRequested { api, .. } = event {
window.hide().unwrap();
api.prevent_close();
}
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
```
### Pattern 3: Configurable Server URL with First-Run Setup
**What:** On first launch, show a local HTML page for server URL input. Store URL persistently.
**When to use:** When desktop app connects to a configurable server (D-02).
```rust
// In setup callback, check if server URL exists
let store = app.store("config.json")?;
let server_url = store.get("server_url");
if let Some(url) = server_url {
// Load the configured server URL
let window = app.get_webview_window("main").unwrap();
window.navigate(url.as_str().unwrap().parse().unwrap());
} else {
// Show local setup page (bundled HTML)
// User enters URL -> JS calls store.set -> navigates to URL
}
```
```javascript
// Source: v2.tauri.app/plugin/store/
// setup.html JavaScript
import { load } from '@tauri-apps/plugin-store';
async function saveAndConnect(url) {
const store = await load('config.json', { autoSave: true });
await store.set('server_url', url);
// Navigate main window to server
window.location.href = url;
}
```
### Pattern 4: Gitea Actions Multi-Stage Pipeline
**What:** CI/CD pipeline triggered on push with staged quality gates.
**When to use:** Automated build/test/deploy workflow.
```yaml
# .gitea/workflows/ci.yml
# Source: docs.gitea.com/usage/actions/ + botmonster.com guide
name: Tessera CI/CD
on:
push:
branches: [main]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm type-check
test:
runs-on: ubuntu-latest
needs: quality
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm test
build-deploy:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- name: Build and deploy
run: |
docker compose build web api
docker compose up -d web api
```
### Anti-Patterns to Avoid
- **Bundling frontend assets in Tauri:** Since Tessera desktop is a wrapper, do NOT copy the Next.js build into the Tauri binary. Use `frontendDist` with a URL. This keeps the desktop app tiny and always shows current server content.
- **Using MSI for Windows builds from Linux:** MSI (WiX) only builds on Windows. Always use NSIS for cross-platform build capability. [CITED: v2.tauri.app/distribute/windows-installer/]
- **Docker-in-Docker for CI builds:** The act_runner already runs inside Docker. Mount the host Docker socket (`/var/run/docker.sock`) instead of running Docker inside Docker. This avoids complexity and security issues. [CITED: docs.gitea.com/usage/actions/act-runner]
- **Complex registry setup:** Since deploy target is the same server, skip pushing to a registry. Build images locally with `docker compose build` and restart services.
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Window state persistence | Manual size/position save/restore | tauri-plugin-window-state | Handles all edge cases (multi-monitor, maximized state, DPI changes) |
| Persistent KV storage | Custom file I/O in Rust | tauri-plugin-store | Handles serialization, atomic writes, cross-platform paths |
| System autostart | Manual registry/plist/desktop-file creation | tauri-plugin-autostart | Cross-platform (Windows registry, Linux .desktop, macOS LaunchAgent) |
| OS notifications | Custom dbus/Win32 notification code | tauri-plugin-notification | Permission handling, cross-platform API, Tauri-integrated |
| CI/CD workflow engine | Custom shell scripts | Gitea Actions | YAML-based, GitHub Actions compatible, Docker-native execution |
| SSH deployment | Manual SSH scripting in CI | appleboy/ssh-action | Handles key auth, connection management, error reporting |
**Key insight:** Every desktop behavior (tray, notifications, autostart, window state) has an official Tauri plugin. The custom Rust code is only ~100 lines of glue connecting these plugins with the close-to-tray behavior and first-run setup flow.
## Common Pitfalls
### Pitfall 1: Missing WebKitGTK Dev Headers
**What goes wrong:** `cargo build` fails with "pkg-config could not find webkit2gtk-4.1"
**Why it happens:** Linux runtime libraries (libwebkit2gtk) are installed but the -dev headers for compilation are not.
**How to avoid:** Install `libwebkit2gtk-4.1-dev` via apt before any Tauri build. The current machine is missing this.
**Warning signs:** `pkg-config --modversion webkit2gtk-4.1` returns "not found"
### Pitfall 2: Tauri App Exits When Window Hidden
**What goes wrong:** App quits entirely when user clicks X, even with close-to-tray configured.
**Why it happens:** Tauri default behavior exits when all windows are destroyed. Hiding a window triggers destroy.
**How to avoid:** Must handle BOTH `WindowEvent::CloseRequested` (call `api.prevent_close()` + `window.hide()`) AND potentially `RunEvent::ExitRequested` (call `api.prevent_exit()`). Both handlers are needed for reliable close-to-tray. [CITED: github.com/tauri-apps/tauri/discussions/2684]
**Warning signs:** App process disappears from task manager after clicking X
### Pitfall 3: frontendDist URL vs. Configurable URL Conflict
**What goes wrong:** `frontendDist` in tauri.conf.json is a static URL, but the app needs a user-configurable server URL.
**Why it happens:** `frontendDist` is a build-time config, not runtime.
**How to avoid:** Set `frontendDist` to a local setup page (or use a custom scheme). On app start in Rust, read the stored URL from tauri-plugin-store and call `window.navigate(url)` to redirect to the actual server. The first-run page handles initial URL configuration. [ASSUMED]
**Warning signs:** All builds hardcoded to one server URL
### Pitfall 4: Gitea Actions docker/build-push-action JWT Parse Error
**What goes wrong:** `docker/build-push-action@v4` fails because Gitea's ACTIONS_RUNTIME_TOKEN is not a JWT.
**Why it happens:** Gitea generates a random string token, not a JWT like GitHub. The action tries to parse it as JWT.
**How to avoid:** Use plain `docker compose build` commands instead of the build-push-action, OR use v6 which may have fixed this. Since we build locally (no registry push), plain commands are simpler anyway. [CITED: docs.gitea.com/usage/actions/comparison]
**Warning signs:** "JWT parse error" in CI logs
### Pitfall 5: act_runner Docker Socket Security
**What goes wrong:** Malicious workflow code could access Docker daemon and inspect other containers.
**Why it happens:** `/var/run/docker.sock` is mounted into the runner container for Docker-in-Docker builds.
**How to avoid:** Since this is an internal-only CI (Claude pushes manually), the risk is acceptable. For extra safety, use ephemeral runners (`GITEA_RUNNER_EPHEMERAL=1`) which revoke credentials after each job. [CITED: docs.gitea.com/usage/actions/act-runner]
**Warning signs:** Runner container has persistent access to Docker daemon
### Pitfall 6: pnpm Workspace Tauri Init Confusion
**What goes wrong:** `tauri init` or `create-tauri-app` runs from wrong directory, creates files in wrong location.
**Why it happens:** Monorepo structure requires precise directory targeting.
**How to avoid:** Create `apps/desktop/` manually, cd into it, run `pnpm create tauri-app .` or `npx tauri init`. Ensure package.json has `name: "@tessera/desktop"`. Add to pnpm-workspace.yaml (already covered by `apps/*` glob). [ASSUMED]
**Warning signs:** src-tauri appears at monorepo root instead of apps/desktop/
## Code Examples
### First-Run Setup Page (Bundled HTML)
```html
Tessera - Setup
```
### Cargo.toml Plugin Dependencies
```toml
# Source: Official Tauri plugin docs
# apps/desktop/src-tauri/Cargo.toml
[dependencies]
tauri = { version = "2", features = ["tray-icon"] }
tauri-plugin-store = "2"
tauri-plugin-notification = "2"
tauri-plugin-autostart = "2"
tauri-plugin-window-state = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
```
### Capabilities Configuration
```json
// Source: v2.tauri.app/plugin/autostart/ + notification/ + store/
// apps/desktop/src-tauri/capabilities/default.json
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Tessera desktop capabilities",
"windows": ["main"],
"permissions": [
"core:default",
"store:default",
"notification:default",
"notification:allow-is-permission-granted",
"notification:allow-request-permission",
"notification:allow-notify",
"autostart:allow-enable",
"autostart:allow-disable",
"autostart:allow-is-enabled",
"window-state:default"
]
}
```
### Version Check Endpoint (API Side)
```typescript
// Source: Pattern for D-08 update hint
// apps/api/src/health/health.controller.ts (extend existing)
@Get('version')
getVersion() {
return {
version: process.env.npm_package_version || '0.0.1',
name: 'tessera',
};
}
```
```rust
// Desktop side: check version on startup
// In setup callback after loading server URL
async fn check_version(server_url: &str, current_version: &str) -> Option {
let url = format!("{}/version", server_url);
if let Ok(resp) = reqwest::get(&url).await {
if let Ok(info) = resp.json::().await {
let remote = info["version"].as_str().unwrap_or("0.0.0");
if remote != current_version {
return Some(remote.to_string());
}
}
}
None
}
```
## State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| Tauri 1.x SystemTray API | Tauri 2.x TrayIconBuilder API | Oct 2024 (Tauri 2.0 stable) | New API: `tauri::tray::TrayIconBuilder`, feature flag renamed `tray-icon` [CITED: v2.tauri.app/blog/tauri-20/] |
| `@tauri-apps/api/notification` | `@tauri-apps/plugin-notification` | Tauri 2.0 | Notifications moved from core API to plugin [CITED: v2.tauri.app/start/migrate/from-tauri-1/] |
| WiX MSI only | NSIS + MSI options | Tauri 2.0 | NSIS is now the recommended Windows installer with cross-compilation support [CITED: v2.tauri.app/distribute/windows-installer/] |
| Gitea without CI | Gitea Actions (since 1.19) | 2023 | Built-in CI/CD, no need for external Jenkins/Drone [CITED: docs.gitea.com/usage/actions/act-runner] |
**Deprecated/outdated:**
- Tauri 1.x APIs: All `SystemTray`, `CustomMenuItem`, `SystemTrayMenu` APIs replaced
- `@tauri-apps/api/notification`: Replaced by `@tauri-apps/plugin-notification`
- `tauri::api::shell`: Removed in Tauri 2, use `tauri-plugin-shell` instead
## Assumptions Log
| # | Claim | Section | Risk if Wrong |
|---|-------|---------|---------------|
| A1 | frontendDist can be overridden at runtime via window.navigate() for configurable URL | Architecture Patterns / Pitfall 3 | Setup flow would need different approach (custom protocol or build-time injection) |
| A2 | appleboy/ssh-action@v1 works with Gitea Actions | Standard Stack (CI/CD) | Would need alternative SSH deployment method (plain script) |
| A3 | docker/build-push-action@v6 compatibility with Gitea Actions | Pitfall 4 | Use plain docker commands instead (already the recommendation) |
| A4 | pnpm create tauri-app works inside existing monorepo subdirectory | Pitfall 6 | May need manual scaffolding of src-tauri directory structure |
| A5 | Tauri tray icon bundle format (.png) works on both Linux and Windows | Code Examples | May need separate icon formats per platform |
| A6 | reqwest crate available for HTTP requests in Tauri Rust backend | Code Examples (version check) | Could use tauri-plugin-http or JS-side fetch instead |
## Open Questions
1. **Backend Notification Events (D-01)**
- What we know: Tauri can send OS notifications via plugin. Desktop app is a WebView wrapper.
- What's unclear: How the backend pushes notification events to the desktop app. WebSocket? SSE? Polling? The web app already runs in the WebView, so standard web push patterns apply.
- Recommendation: Use the web app's existing event mechanism (if any) or add a simple polling endpoint. Full WebSocket/SSE for notifications can be deferred since the web app itself would also benefit from it.
2. **First-Run Setup Page Architecture**
- What we know: Need to show a local page for URL input before loading external URL.
- What's unclear: Whether to use a bundled HTML file, a custom Tauri protocol, or handle it entirely in Rust with a simple dialog.
- Recommendation: Bundle a minimal `setup.html` that uses tauri-plugin-store. Set `frontendDist` to this page. After URL is saved, navigate to the server URL.
3. **Gitea Instance Access**
- What we know: The project has no git remotes configured. Gitea should be on the same server.
- What's unclear: Whether Gitea is already running, its URL, and whether Actions are enabled.
- Recommendation: Document Gitea setup as a prerequisite. If not running, add to docker-compose or assume existing instance.
## Environment Availability
| Dependency | Required By | Available | Version | Fallback |
|------------|------------|-----------|---------|----------|
| Rust toolchain | Tauri compilation | **NO** | -- | Must install via rustup |
| libwebkit2gtk-4.1-dev | Tauri Linux build | **NO** | -- | Must install via apt |
| libssl-dev | Tauri dependencies | Check needed | -- | Must install via apt |
| libxdo-dev | Tauri Linux features | Check needed | -- | Must install via apt |
| build-essential | Compilation | Check needed | -- | Must install via apt |
| Node.js | Frontend tooling | YES | v24.16.0 | -- |
| pnpm | Package manager | YES | 9.15.0 | -- |
| Docker | Container builds | YES | 29.5.3 | -- |
| Docker Compose | Service orchestration | YES | v5.1.4 | -- |
| GTK3 (runtime) | Tauri runtime | YES | 3.24.49 | -- |
| librsvg2 (runtime) | Icon rendering | YES | 2.60.0 | -- |
| libayatana-appindicator3 (runtime) | System tray | YES | 0.5.94 | -- |
| Gitea instance | CI/CD | Unknown | -- | Must verify or set up |
| act_runner | CI job execution | Unknown | -- | Add to docker-compose |
**Missing dependencies with no fallback:**
- Rust toolchain (rustc, cargo) -- MUST install before any Tauri work
- libwebkit2gtk-4.1-dev -- MUST install for Linux compilation
- Other -dev packages (libssl-dev, libxdo-dev, build-essential) -- MUST install
**Missing dependencies with fallback:**
- Gitea instance -- if not available, can defer CI/CD track or add to docker-compose
## Validation Architecture
### Test Framework
| Property | Value |
|----------|-------|
| Framework | Vitest 3.x |
| Config file | `apps/web/vitest.config.ts` (existing) |
| Quick run command | `pnpm --filter=@tessera/web test` |
| Full suite command | `pnpm test` |
### Phase Requirements to Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| DESK-01 | Tauri app builds for Linux (AppImage) and Windows (NSIS) | manual | `cd apps/desktop && pnpm tauri build` | N/A -- build verification |
| DESK-02 | Desktop connects to web backend via URL | manual | Verify WebView loads configured URL | N/A -- runtime verification |
| INFRA-04 | CI pipeline runs on push | integration | Push to Gitea, verify Actions run | N/A -- infrastructure verification |
| D-02 | Server URL stored persistently | smoke | Launch app, set URL, restart, verify URL retained | N/A -- manual smoke test |
| D-03 | Close minimizes to tray | smoke | Click X, verify tray icon, verify process alive | N/A -- manual smoke test |
| D-05 | Window state persisted | smoke | Resize, close, reopen, verify dimensions | N/A -- manual smoke test |
### Sampling Rate
- **Per task commit:** `pnpm lint && pnpm type-check` (no Tauri-specific unit tests expected)
- **Per wave merge:** `pnpm test` (full suite)
- **Phase gate:** Manual smoke test of desktop app + CI pipeline run
### Wave 0 Gaps
- No automated tests planned for desktop wrapper -- Tauri apps are best validated via manual smoke testing (WebView behavior, tray interaction, OS-level features)
- CI pipeline validation requires a running Gitea instance
- Existing web/api tests should continue passing (regression)
## Security Domain
### Applicable ASVS Categories
| ASVS Category | Applies | Standard Control |
|---------------|---------|-----------------|
| V2 Authentication | no | Auth handled by web app (Phase 2), not desktop shell |
| V3 Session Management | no | Sessions managed by web app cookies in WebView |
| V4 Access Control | no | Access control in NestJS API layer |
| V5 Input Validation | yes | Server URL input validation (URL format, https preference) |
| V6 Cryptography | no | No crypto in desktop wrapper |
### Known Threat Patterns for Tauri Desktop
| Pattern | STRIDE | Standard Mitigation |
|---------|--------|---------------------|
| Malicious server URL injection | Tampering | Validate URL format, warn on non-HTTPS, store in plugin-store (not plaintext config) |
| WebView navigation hijacking | Spoofing | Lock WebView to configured server domain via CSP in tauri.conf.json |
| Docker socket exposure (CI) | Elevation | Ephemeral runners, internal-only CI, no untrusted code |
| CI secrets leakage | Information Disclosure | Use Gitea secrets store, never log secrets in pipeline output |
## Sources
### Primary (HIGH confidence)
- [v2.tauri.app/start/prerequisites/](https://v2.tauri.app/start/prerequisites/) - Linux system dependencies
- [v2.tauri.app/reference/config/](https://v2.tauri.app/reference/config/) - Configuration reference (frontendDist, windows, bundle)
- [v2.tauri.app/learn/system-tray/](https://v2.tauri.app/learn/system-tray/) - TrayIconBuilder API, menu events
- [v2.tauri.app/plugin/store/](https://v2.tauri.app/plugin/store/) - Plugin store API
- [v2.tauri.app/plugin/notification/](https://v2.tauri.app/plugin/notification/) - Notification plugin
- [v2.tauri.app/plugin/autostart/](https://v2.tauri.app/plugin/autostart/) - Autostart plugin
- [v2.tauri.app/plugin/window-state/](https://v2.tauri.app/plugin/window-state/) - Window state plugin
- [v2.tauri.app/distribute/windows-installer/](https://v2.tauri.app/distribute/windows-installer/) - NSIS vs MSI
- [docs.gitea.com/usage/actions/act-runner](https://docs.gitea.com/usage/actions/act-runner) - Runner setup
- [docs.gitea.com/usage/actions/comparison](https://docs.gitea.com/usage/actions/comparison) - Gitea vs GitHub Actions
### Secondary (MEDIUM confidence)
- [npm registry](https://www.npmjs.com/package/@tauri-apps/cli) - Package versions verified
- [melvinoostendorp.nl/blog/tauri-v2-nextjs-monorepo-guide](https://melvinoostendorp.nl/blog/tauri-v2-nextjs-monorepo-guide) - Monorepo structure reference
- [botmonster.com/posts/self-hosted-cicd-pipeline-gitea-actions-docker/](https://botmonster.com/posts/self-hosted-cicd-pipeline-gitea-actions-docker/) - Gitea Actions Docker pipeline example
- [github.com/tauri-apps/tauri/discussions/2684](https://github.com/tauri-apps/tauri/discussions/2684) - Close-to-tray pattern
### Tertiary (LOW confidence)
- Training knowledge for frontendDist runtime override via window.navigate() (A1)
- Training knowledge for reqwest crate usage in Tauri (A6)
## Project Constraints (from CLAUDE.md)
- **Docker-based infrastructure:** All components as containers. CI/CD deploys via docker-compose.
- **Mandantenfaehigkeit:** Multi-tenancy architecture. Desktop wrapper is tenant-agnostic (URL-based).
- **Claude builds everything:** Architecture must be understandable. Desktop app code should be minimal Rust.
- **pnpm workspaces:** Desktop app integrates as `apps/desktop` in existing monorepo structure.
- **Biome for linting:** CI pipeline must run Biome (not ESLint) for lint step.
- **Vitest for tests:** CI pipeline uses Vitest, matching existing test infrastructure.
- **TypeScript 5.5+:** Type-check step in CI uses existing turbo task.
## Metadata
**Confidence breakdown:**
- Standard stack: MEDIUM - All packages verified on npm registry and documented on official Tauri docs. Tauri 2 is stable (released Oct 2024). Package versions confirmed.
- Architecture: MEDIUM - URL-loading wrapper pattern is documented. Close-to-tray pattern confirmed via multiple community examples. First-run setup flow uses assumed runtime URL override.
- Pitfalls: HIGH - WebKitGTK dependency confirmed missing via environment probe. JWT parse error documented in Gitea Actions comparison docs.
**Research date:** 2026-06-25
**Valid until:** 2026-07-25 (stable technology, 30-day validity)