Files
tessera-ctl/.planning/phases/06-desktop-client-ci-cd/06-RESEARCH.md
T

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
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 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)

<!-- 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, 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)

Secondary (MEDIUM confidence)

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)