agentsclimarketplace

Desktop backend tauri

Skill agents-inc/skills/dist/plugins/desktop-backend-tauri/skills/desktop-backend-tauri

The official skills marketplace for Agents Inc, an agent composition framework that builds stacks and compiles specialized subagents for Claude Code

Install
npx -y skills add agents-inc/skills --skill desktop-backend-tauri

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 18 stars18 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Tauri 2.x Rust command patterns, state management, error handling, events, channels, testing

SKILL.md

16.1 KB, ~3.9k tokens by cl100k_base, as published. Nobody here has run it

Tauri Rust Backend Patterns

Quick Guide: Define commands with #[tauri::command], register in generate_handler![]. Use State<T> for shared state (wrap mutable fields in Mutex). Error types must implement both serde::Serialize and Display -- use thiserror for ergonomic error enums. Async commands run on Tokio -- borrowed args (&str, State<'_, T>) require Result<T, E> return type. Stream data to frontend via Channel<T> (not events) for high throughput. Emit events with app.emit() for fire-and-forget notifications.

Current version: Tauri 2.x (stable). Async runtime is Tokio.


<critical_requirements>

CRITICAL: Before Using This Skill

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)

(You MUST register every command in tauri::generate_handler![] -- unregistered commands compile fine but silently fail at runtime)

(You MUST implement serde::Serialize on all error types returned from commands -- Tauri serializes errors across the IPC boundary)

(You MUST wrap mutable managed state in Mutex or RwLock -- commands run concurrently and State<T> requires Send + Sync)

(You MUST return Result<T, E> from async commands that use borrowed args (&str, State<'_, T>) -- Rust lifetime rules require it)

(You MUST use Channel<T> for streaming data to frontend -- events are designed for small payloads, not high-throughput streaming)

</critical_requirements>


Auto-detection: #[tauri::command], tauri::command, tauri::State, AppHandle, app.manage, generate_handler, tauri::ipc::Channel, Emitter, Listener, thiserror, tauri::test, mock_builder, async tauri command, tauri error handling, tauri state management

When to use:

  • Defining Rust command handlers (sync and async) for frontend invocation
  • Managing application state across commands with app.manage() and State<T>
  • Implementing error types that serialize across the IPC boundary
  • Emitting events from Rust to frontend (progress, notifications, background updates)
  • Streaming data from Rust to frontend via channels
  • Testing Rust commands with Tauri's mock runtime
  • Organizing commands into modules as the backend grows

When NOT to use:

  • Frontend invoke patterns and TypeScript types (see the framework-level Tauri skill)
  • Permission/capability configuration (see the framework-level Tauri skill)
  • Plugin installation and configuration (see the framework-level Tauri skill)
  • Window management, system tray, menus (see the framework-level Tauri skill)
  • Packaging and distribution (see the framework-level Tauri skill)
  • General Rust programming not specific to Tauri APIs

Key patterns covered:

Detailed resources:


<philosophy>

Philosophy

The Tauri Rust backend is the trust boundary between the untrusted webview frontend and the operating system. Every sensitive operation -- file I/O, network requests, shell commands, state mutations -- flows through Rust commands. The backend is responsible for validation, authorization, and safe execution.

Design principles:

  • Commands are the API surface. Each command is a well-defined endpoint with typed arguments, typed return values, and explicit error handling. Treat them like HTTP handlers.
  • State is managed, not global. Use app.manage(T) to register singletons. Commands request state via State<T> injection -- no global statics, no lazy_static.
  • Errors are data, not panics. Never unwrap() in commands. Return Result<T, E> where E implements Serialize. The frontend receives structured error information.
  • Async by default for I/O. Sync commands block the main thread. Use async for anything involving files, network, or long computation. Tokio is the runtime.
  • Channels for streaming, events for notifications. Channel<T> is optimized for ordered, high-throughput data delivery. Events are pub-sub fire-and-forget for small payloads.
</philosophy>
<patterns>

Core Patterns

Pattern 1: Sync and Async Commands

Sync commands execute on the main thread. Async commands run on Tokio's thread pool.

// Sync -- blocks main thread, use only for fast operations
#[tauri::command]
fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
}

// Async -- runs on Tokio, use for I/O and long operations
#[tauri::command]
async fn read_file(path: String) -> Result<String, String> {
    tokio::fs::read_to_string(&path)
        .await
        .map_err(|e| e.to_string())
}

Key rule: Async commands cannot use &str arguments unless the return type is Result<T, E>. Use String for owned args, or wrap in Result to satisfy Rust's async lifetime constraints.

See examples/core.md for command registration and argument conventions.


Pattern 2: Error Handling with thiserror

Command error types must implement both Serialize (for IPC) and Display (for Tauri's error serialization). The thiserror crate provides Display via #[error()] macros; implement Serialize manually to serialize as a string.

use thiserror::Error;

#[derive(Debug, Error)]
enum AppError {
    #[error("File not found: {0}")]
    NotFound(String),
    #[error(transparent)]
    Io(#[from] std::io::Error),
    #[error("Validation failed: {0}")]
    Validation(String),
}

// Manual Serialize -- converts error to its Display string
impl serde::Serialize for AppError {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::ser::Serializer,
    {
        serializer.serialize_str(self.to_string().as_ref())
    }
}

Why manual Serialize: #[derive(Serialize)] on error enums serializes the enum variant structure (e.g., {"Io": {...}}), which is rarely useful for frontend error display. Serializing as a string gives the frontend a human-readable message.

See examples/core.md for the full error pattern with #[from] conversions.


Pattern 3: Managed State with Mutex

Register state with app.manage(). Commands access it via State<T> injection. Mutable fields require Mutex or RwLock.

use std::sync::Mutex;

#[derive(Default)]
struct AppState {
    counter: Mutex<u32>,
    config: Mutex<AppConfig>,
}

#[tauri::command]
fn increment(state: tauri::State<AppState>) -> u32 {
    let mut counter = state.counter.lock().unwrap();
    *counter += 1;
    *counter
}

Key rule: State<T> requires T: Send + Sync. Mutex<T> and RwLock<T> provide this for mutable data. Tauri injects state automatically -- it is not passed from the frontend. Missing .manage() registration causes a runtime panic.

See examples/core.md for async state access, type alias patterns, and RwLock usage.


Pattern 4: AppHandle for App Resources

AppHandle gives commands access to the app's runtime: paths, windows, event emission, and plugin APIs.

use tauri::Manager;

#[tauri::command]
async fn get_app_data_path(app: tauri::AppHandle) -> Result<String, String> {
    app.path()
        .app_data_dir()
        .map(|p| p.to_string_lossy().into_owned())
        .map_err(|e| e.to_string())
}

Key rule: AppHandle is injected automatically like State<T>. Import tauri::Manager to access .path(), .get_webview_window(), and other runtime methods.

See examples/core.md for window access and combined state + AppHandle patterns.


Pattern 5: Channels for Streaming

Channel<T> streams ordered data from a command to the frontend. More efficient than events for high-throughput scenarios (file reads, download progress, log streaming).

use tauri::ipc::Channel;
use serde::Serialize;

#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase", tag = "type")]
enum DownloadEvent {
    #[serde(rename_all = "camelCase")]
    Progress { percent: u32, bytes_received: u64 },
    Finished,
}

#[tauri::command]
async fn download(url: String, on_event: Channel<DownloadEvent>) -> Result<(), String> {
    // ... download logic ...
    on_event.send(DownloadEvent::Progress { percent: 50, bytes_received: 1024 })
        .map_err(|e| e.to_string())?;
    on_event.send(DownloadEvent::Finished)
        .map_err(|e| e.to_string())?;
    Ok(())
}

Key rule: Channel payload types must implement Serialize + Clone. The channel is tied to the command invocation lifecycle. Use events (not channels) when you need to broadcast to all listeners from outside a command.

See examples/core.md for the frontend Channel setup.


Pattern 6: Emitting Events from Rust

Events provide fire-and-forget pub-sub communication from backend to frontend. Use for progress notifications, background updates, and decoupled messaging.

use tauri::Emitter;

#[tauri::command]
async fn start_sync(app: tauri::AppHandle) -> Result<(), String> {
    app.emit("sync-started", ()).map_err(|e| e.to_string())?;
    // ... sync work ...
    app.emit("sync-complete", serde_json::json!({ "count": 42 }))
        .map_err(|e| e.to_string())?;
    Ok(())
}

Key rule: Import tauri::Emitter to use .emit(), .emit_to(), and .emit_filter(). Event payloads must implement Serialize + Clone. Events are not typed -- use consistent naming conventions.

See examples/events.md for targeted window events, filtered emission, and listening from Rust.

</patterns>

<decision_framework>

Decision Framework

Command Design

How should this command be structured?
|-- Fast, CPU-only, no I/O?
|   +-- Sync command: #[tauri::command] fn
|-- Involves file, network, or long computation?
|   +-- Async command: #[tauri::command] async fn -> Result<T, E>
|-- Needs shared app state?
|   +-- Add State<T> parameter, register with .manage()
|-- Needs app paths, windows, or event emission?
|   +-- Add AppHandle parameter, import Manager trait
|-- Needs to stream data back to frontend?
|   +-- Add Channel<T> parameter
+-- Needs raw request headers or binary body?
    +-- Add tauri::ipc::Request parameter

Communication Method

How should Rust communicate with the frontend?
|-- Request/response (frontend asks, Rust answers)?
|   +-- Command (invoke from frontend, return value)
|-- Ordered stream from a specific operation?
|   +-- Channel<T> parameter in a command
|-- Fire-and-forget notification (broadcast)?
|   +-- Event: app.emit() or app.emit_to()
+-- Need to run JS in the webview?
    +-- webview.eval() (escape hatch, avoid if possible)

Error Strategy

How should this command handle errors?
|-- Quick prototype or simple command?
|   +-- Result<T, String> with .map_err(|e| e.to_string())
|-- Production command with multiple error sources?
|   +-- Custom error enum with thiserror + manual Serialize impl
|-- Truly unrecoverable (corrupt state, invariant violation)?
|   +-- panic! (but never unwrap() on expected errors)

State Mutability

How should state be wrapped?
|-- Read-only config set once at startup?
|   +-- No wrapper needed: app.manage(Config { ... })
|-- Read-heavy, infrequent writes?
|   +-- RwLock<T>: multiple concurrent readers, exclusive writer
|-- Frequent reads and writes, simple fields?
|   +-- Mutex<T>: exclusive access for both reads and writes
+-- Need to hold lock across .await points?
    +-- tokio::sync::Mutex (not std::sync::Mutex)

</decision_framework>


<red_flags>

RED FLAGS

High Priority Issues:

  • Using unwrap() in commands instead of returning Result -- panics crash the command handler, frontend gets a generic error with no details
  • Forgetting to register commands in generate_handler![] -- compiles fine, silently fails at runtime
  • Missing serde::Serialize on error types -- compilation error, but the fix is non-obvious (manual impl, not derive)
  • Using std::sync::Mutex and holding the lock across .await -- blocks the Tokio runtime, causes deadlocks. Use tokio::sync::Mutex when you need to hold across await points
  • Forgetting .manage(T) registration -- runtime panic when a command tries to access State<T>
  • Deriving Serialize on error enums -- produces variant-structure JSON ({"Io": {...}}) instead of a readable string

Medium Priority Issues:

  • Using events for high-throughput streaming (download progress, log tailing) -- events are JSON-serialized pub-sub, not optimized for throughput. Use Channel<T>
  • Using sync commands for I/O operations -- blocks the main thread, freezes the webview
  • Not importing tauri::Emitter when calling .emit() -- compilation error with confusing message about missing method
  • Returning Option<()> from commands -- serializes as null which the frontend may not expect (serde serialization/deserialization asymmetry)

Gotchas & Edge Cases:

  • Async + borrowed args: async fn cmd(name: &str) without Result return type fails to compile. Either use String or return Result<T, E>
  • Argument naming: Frontend passes camelCase (invokeMessage), Rust receives snake_case (invoke_message) by default. Use #[tauri::command(rename_all = "snake_case")] to change this
  • State injection order: State<T> parameters are not passed from frontend -- they are injected by Tauri. Mixing up "frontend args" and "injected params" in the function signature is confusing but works (Tauri filters them)
  • Mutex poisoning: lock().unwrap() panics if a previous holder panicked. In production, handle PoisonError or use lock().expect("state lock poisoned")
  • Multiple state types: Each .manage(T) call registers a separate type. State<Mutex<AppState>> and State<AppState> are different registrations
  • Channel lifetime: Channel<T> is tied to the command invocation. It cannot be stored for later use outside the command
  • Event payload types: Event payloads must be Serialize + Clone. serde_json::Value works as a catch-all but loses type safety
  • emit_to target: Target is a webview label string. If the webview does not exist, the event is silently dropped

</red_flags>


<critical_reminders>

CRITICAL REMINDERS

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)

(You MUST register every command in tauri::generate_handler![] -- unregistered commands compile fine but silently fail at runtime)

(You MUST implement serde::Serialize on all error types returned from commands -- Tauri serializes errors across the IPC boundary)

(You MUST wrap mutable managed state in Mutex or RwLock -- commands run concurrently and State<T> requires Send + Sync)

(You MUST return Result<T, E> from async commands that use borrowed args (&str, State<'_, T>) -- Rust lifetime rules require it)

(You MUST use Channel<T> for streaming data to frontend -- events are designed for small payloads, not high-throughput streaming)

Failure to follow these rules will cause silent command failures, runtime panics, deadlocked async runtimes, or unserializable error types.

</critical_reminders>

What ships with it: 4 files

28.4 KB alongside SKILL.md

examples/

Gives 0 of the 12 instructions most data backend skills give in ~3.9k tokens

Counted across 229 of the 229 authors here whose files we hold, read 2026-08-07

  • Separate business logic into service layersin 22 of 229, across 15 files
  • Retry failures with exponential backoffin 21 of 229, across 14 files
  • Select only needed database columnsin 20 of 229, across 13 files
  • Abstract data access into repository classesin 19 of 229, across 12 files
  • Use centralized error handlersin 17 of 229, across 10 files
  • Use AsNoTracking for read-only queriesin 16 of 229, across 4 files
  • Use async/await for all I/O operationsin 16 of 229, across 5 files
  • Implement structured loggingin 15 of 229, across 4 files
  • Use dependency injection for all servicesin 14 of 229, across 2 files
  • Use resource-based URLs for REST APIsin 13 of 229, across 7 files
  • Invalidate cache after data changesin 13 of 229, across 9 files
  • Use a dependency injection containerin 12 of 229, across 4 files

Said here and by no other author read

  • Register every command in the handler macro
  • Follow project conventions in CLAUDE.md
  • Implement Serialize and Display on error types
  • Wrap mutable state fields in Mutex or RwLock
  • Return Result for async commands with borrowed args
  • Use Channel for streaming high throughput data

Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.

Keep looking

Skills are one crate of 327,069. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.