40 KiB
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>
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
</user_constraints>
<phase_requirements>
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 |
</phase_requirements>
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):
# 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.
// 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.
// 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).
// 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
}
// 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.
# .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
frontendDistwith 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 buildand 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)
<!-- Source: Pattern derived from tauri-plugin-store docs -->
<!-- apps/desktop/src/setup.html -->
<!DOCTYPE html>
<html>
<head>
<title>Tessera - Setup</title>
<style>
body { font-family: system-ui; display: flex; justify-content: center;
align-items: center; min-height: 100vh; margin: 0;
background: oklch(0.17 0.01 260); color: white; }
.setup { text-align: center; max-width: 400px; }
input { width: 100%; padding: 12px; border-radius: 8px; border: 1px solid #555;
background: #222; color: white; font-size: 16px; margin: 16px 0; }
button { padding: 12px 32px; border-radius: 8px; border: none;
background: oklch(0.85 0.17 85); color: black; cursor: pointer;
font-size: 16px; font-weight: 600; }
</style>
</head>
<body>
<div class="setup">
<h1>Tessera</h1>
<p>Server-URL eingeben:</p>
<input id="url" type="url" placeholder="https://tessera.example.com"
value="http://localhost:3000" />
<button onclick="connect()">Verbinden</button>
</div>
<script type="module">
import { load } from '@tauri-apps/plugin-store';
window.connect = async () => {
const url = document.getElementById('url').value.trim();
if (!url) return;
const store = await load('config.json', { autoSave: true });
await store.set('server_url', url);
window.location.href = url;
};
</script>
</body>
</html>
Cargo.toml Plugin Dependencies
# 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
// 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)
// 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',
};
}
// Desktop side: check version on startup
// In setup callback after loading server URL
async fn check_version(server_url: &str, current_version: &str) -> Option<String> {
let url = format!("{}/version", server_url);
if let Ok(resp) = reqwest::get(&url).await {
if let Ok(info) = resp.json::<serde_json::Value>().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,SystemTrayMenuAPIs replaced @tauri-apps/api/notification: Replaced by@tauri-apps/plugin-notificationtauri::api::shell: Removed in Tauri 2, usetauri-plugin-shellinstead
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
-
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.
-
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.htmlthat uses tauri-plugin-store. SetfrontendDistto this page. After URL is saved, navigate to the server URL.
-
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/ - Linux system dependencies
- v2.tauri.app/reference/config/ - Configuration reference (frontendDist, windows, bundle)
- v2.tauri.app/learn/system-tray/ - TrayIconBuilder API, menu events
- v2.tauri.app/plugin/store/ - Plugin store API
- v2.tauri.app/plugin/notification/ - Notification plugin
- v2.tauri.app/plugin/autostart/ - Autostart plugin
- v2.tauri.app/plugin/window-state/ - Window state plugin
- v2.tauri.app/distribute/windows-installer/ - NSIS vs MSI
- docs.gitea.com/usage/actions/act-runner - Runner setup
- docs.gitea.com/usage/actions/comparison - Gitea vs GitHub Actions
Secondary (MEDIUM confidence)
- npm registry - Package versions verified
- melvinoostendorp.nl/blog/tauri-v2-nextjs-monorepo-guide - Monorepo structure reference
- botmonster.com/posts/self-hosted-cicd-pipeline-gitea-actions-docker/ - Gitea Actions Docker pipeline example
- 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/desktopin 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)