Claude Agent Skill · by Nodnarbnitram

Tauri V2

This does the heavy lifting for Tauri v2 development, where the permission system and mobile builds trip up most developers. It catches the classic mistakes lik

Install
Terminal · npx
$npx skills add https://github.com/nodnarbnitram/claude-code-extensions --skill tauri-v2
Works with Paperclip

How Tauri V2 fits into a Paperclip company.

Tauri V2 drops into any Paperclip agent that handles this kind of work. Assign it to a specialist inside a pre-configured PaperclipOrg company and the skill becomes available on every heartbeat — no prompt engineering, no tool wiring.

S
SaaS FactoryPaired

Pre-configured AI company — 18 agents, 18 skills, one-time purchase.

$27$59
Explore pack
Source file
SKILL.md497 lines
Expand
---name: tauri-v2description: "Tauri v2+ cross-platform app development with Rust backend. Use when configuring tauri.conf.json, implementing Rust commands (#[tauri::command]), setting up IPC patterns (invoke, emit, channels), configuring permissions/capabilities, troubleshooting build issues, or deploying desktop/mobile apps. Triggers on Tauri, src-tauri, invoke, emit, capabilities.json."version: 1.0.1--- # Tauri v2+ Development Skill > Build cross-platform desktop and mobile apps with web frontends and Rust backends. ## Before You Start **This skill prevents 8+ common errors and saves ~60% tokens.** | Metric | Without Skill | With Skill ||--------|--------------|------------|| Setup Time | ~2 hours | ~30 min || Common Errors | 8+ | 0 || Token Usage | High (exploration) | Low (direct patterns) | ### Known Issues This Skill Prevents 1. Permission denied errors from missing capabilities2. IPC failures from unregistered commands in `generate_handler!`3. State management panics from type mismatches4. Mobile build failures from missing Rust targets5. White screen issues from misconfigured dev URLs ## Quick Start ### Step 1: Create a Tauri Command ```rust// src-tauri/src/lib.rs#[tauri::command]fn greet(name: String) -> String {    format!("Hello, {}!", name)} #[cfg_attr(mobile, tauri::mobile_entry_point)]pub fn run() {    tauri::Builder::default()        .invoke_handler(tauri::generate_handler![greet])        .run(tauri::generate_context!())        .expect("error while running tauri application");}``` **Why this matters:** Commands not in `generate_handler![]` silently fail when invoked from frontend. > **`main.rs` stays thin:** `src-tauri/src/main.rs` should only be a thin passthrough — all application logic lives in `lib.rs`:> ```rust> // src-tauri/src/main.rs> #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]> fn main() {>     app_lib::run();> }> ```> This split is required for mobile builds — Tauri replaces `main()` with `mobile_entry_point` on mobile targets. ### Step 2: Call from Frontend ```typescriptimport { invoke } from '@tauri-apps/api/core'; const greeting = await invoke<string>('greet', { name: 'World' });console.log(greeting); // "Hello, World!"``` **Why this matters:** Use `@tauri-apps/api/core` (not `@tauri-apps/api/tauri` - that's v1 API). ### Step 3: Add Required Permissions ```json// src-tauri/capabilities/default.json{    "$schema": "../gen/schemas/desktop-schema.json",    "identifier": "default",    "windows": ["main"],    "permissions": ["core:default"]}``` **Why this matters:** Tauri v2 denies everything by default - explicit permissions required for all operations. ## Critical Rules ### Always Do - Register every command in `tauri::generate_handler![cmd1, cmd2, ...]`- Return `Result<T, E>` from commands for proper error handling- Use `Mutex<T>` for shared state accessed from multiple commands- Add capabilities before using any plugin features- Use `lib.rs` for shared code (required for mobile builds)- Use `#[cfg_attr(mobile, tauri::mobile_entry_point)]` on `pub fn run()` in `lib.rs` for mobile compatibility ### Never Do - Never use borrowed types (`&str`) in async commands - use owned types- Never block the main thread - use async for I/O operations- Never hardcode paths - use Tauri path APIs (`app.path()`)- Never skip capability setup - even "safe" operations need permissions ### Common Mistakes **Wrong - Borrowed type in async:**```rust#[tauri::command]async fn bad(name: &str) -> String { // Compile error!    name.to_string()}``` **Correct - Owned type:**```rust#[tauri::command]async fn good(name: String) -> String {    name}``` **Why:** Async commands cannot borrow data across await points; Tauri requires owned types for async command parameters. ## Known Issues Prevention | Issue | Root Cause | Solution ||-------|-----------|----------|| "Command not found" | Missing from `generate_handler!` | Add command to handler macro || "Permission denied" | Missing capability | Add to `capabilities/default.json` || Plugin feature silently fails | Plugin installed but permission not in capability | Add plugin permission string to `capabilities/default.json` || Updater fails in production | Unsigned artifacts or HTTP endpoint | Generate keys with `cargo tauri signer generate`, use HTTPS endpoint only || Sidecar not found | `externalBin` not in `tauri.conf.json` or missing executable | Add path to `bundle.externalBin`, ensure binary is bundled || Feature works on desktop, breaks on mobile | Desktop-only API used | Check if API has mobile support — some plugins are desktop-only || State panic on access | Type mismatch in `State<T>` | Use exact type from `.manage()` || White screen on launch | Frontend not building | Check `beforeDevCommand` in config || IPC timeout | Blocking async command | Remove blocking code or use spawn || Mobile build fails | Missing Rust targets | Run `rustup target add <target>` | ## Deep-Dive References - **Security & permissions** → [`references/capabilities-reference.md`](references/capabilities-reference.md)- **IPC decision guide** → [`references/ipc-patterns.md`](references/ipc-patterns.md)- **Official plugins** → [`references/plugin-reference.md`](references/plugin-reference.md)- **Updater & distribution** → [`references/updater-distribution-reference.md`](references/updater-distribution-reference.md)- **Tray, sidecars, deep links** → [`references/advanced-runtime-reference.md`](references/advanced-runtime-reference.md) ## Configuration Reference ### tauri.conf.json ```json{    "$schema": "./gen/schemas/desktop-schema.json",    "productName": "my-app",    "version": "1.0.0",    "identifier": "com.example.myapp",    "build": {        "devUrl": "http://localhost:5173",        "frontendDist": "../dist",        "beforeDevCommand": "npm run dev",        "beforeBuildCommand": "npm run build"    },    "app": {        "windows": [{            "label": "main",            "title": "My App",            "width": 800,            "height": 600        }],        "security": {            "csp": "default-src 'self'; img-src 'self' data:",            "capabilities": ["default"]        }    },    "bundle": {        "active": true,        "targets": "all",        "icon": ["icons/icon.icns", "icons/icon.ico", "icons/icon.png"]    }}``` **Key settings:**- `build.devUrl`: Must match your frontend dev server port- `app.security.capabilities`: Array of capability file identifiers **Plugin configuration** — Some plugins require additional `tauri.conf.json` blocks (e.g., `store`, `updater`). Always check the specific plugin docs at `v2.tauri.app/plugin/<plugin-name>/` for required config keys. ## Project Structure ```my-tauri-app/├── src/                    # Frontend source├── src-tauri/│   ├── src/│   │   ├── main.rs         # Thin passthrough — calls lib::run()│   │   └── lib.rs          # ALL application logic lives here│   ├── capabilities/│   │   └── default.json    # Capability definitions (grant permissions here)│   ├── tauri.conf.json     # App configuration (devUrl, bundle, security)│   ├── Cargo.toml          # Rust dependencies│   └── build.rs            # Build script (required for tauri-build)└── package.json``` **Why `lib.rs` owns all logic:** Tauri replaces `main()` with `#[cfg_attr(mobile, tauri::mobile_entry_point)]` on mobile. All commands, state, and builder setup must live in `lib.rs::run()`. ### Cargo.toml ```toml[package]name = "app"version = "0.1.0"edition = "2021" [lib]name = "app_lib"crate-type = ["staticlib", "cdylib", "rlib"] [build-dependencies]tauri-build = { version = "2", features = [] } [dependencies]tauri = { version = "2", features = [] }serde = { version = "1", features = ["derive"] }serde_json = "1"``` **Key settings:**- `[lib]` section: Required for mobile builds- `crate-type`: Must include all three types for cross-platform ## Common Patterns ### Error Handling Pattern Use `Result<T, E>` and `thiserror` for type-safe error propagation across the IPC boundary. See [`references/ipc-patterns.md`](references/ipc-patterns.md) for full implementation details. ```rustuse thiserror::Error; #[derive(Debug, Error)]enum AppError {    #[error("IO error: {0}")]    Io(#[from] std::io::Error),    #[error("Not found: {0}")]    NotFound(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())    }} #[tauri::command]fn risky_operation() -> Result<String, AppError> {    Ok("success".into())}``` ### Serde Boundary Rules All command arguments must implement `serde::Deserialize`, and return types must implement `serde::Serialize`. This is how Tauri bridges JSON over the IPC boundary. ```rustuse serde::{Deserialize, Serialize}; #[derive(Deserialize)]struct CreateUserArgs {    name: String,    email: String,    role: Option<String>,  // Optional fields use Option<T>} #[derive(Serialize)]struct User {    id: u64,    name: String,} #[tauri::command]fn create_user(args: CreateUserArgs) -> Result<User, String> {    Ok(User { id: 1, name: args.name })}``` **Common serde pitfalls:**- Field names are camelCase in JS, snake_case in Rust — Tauri automatically converts between them- `Option<T>` maps to optional JS arguments (can be `undefined` or `null`)- Complex enums need `#[serde(tag = "type")]` or similar to be JSON-safe- Error types must also implement `Serialize` (see Error Handling Pattern above) ### State Management Pattern Tauri state manages application data across commands. See [`references/ipc-patterns.md`](references/ipc-patterns.md) for more complex state patterns. ```rustuse std::sync::Mutex;use tauri::State; struct AppState {    counter: u32,} #[tauri::command]fn increment(state: State<'_, Mutex<AppState>>) -> u32 {    let mut s = state.lock().unwrap();    s.counter += 1;    s.counter} // In builder:tauri::Builder::default()    .manage(Mutex::new(AppState { counter: 0 }))``` ### Event Emission Pattern Events are fire-and-forget notifications. See [`references/ipc-patterns.md`](references/ipc-patterns.md) for bidirectional examples. ```rustuse tauri::Emitter; #[tauri::command]fn start_task(app: tauri::AppHandle) {    std::thread::spawn(move || {        app.emit("task-progress", 50).unwrap();        app.emit("task-complete", "done").unwrap();    });}``` ```typescriptimport { listen } from '@tauri-apps/api/event'; const unlisten = await listen('task-progress', (e) => {    console.log('Progress:', e.payload);});// Call unlisten() when done``` ### Channel Streaming Pattern Channels provide high-frequency, typed streaming from Rust to Frontend. See [`references/ipc-patterns.md`](references/ipc-patterns.md) for full implementation details. ```rustuse tauri::ipc::Channel; #[derive(Clone, serde::Serialize)]#[serde(tag = "event", content = "data")]enum DownloadEvent {    Progress { percent: u32 },    Complete { path: String },} #[tauri::command]async fn download(url: String, on_event: Channel<DownloadEvent>) {    for i in 0..=100 {        on_event.send(DownloadEvent::Progress { percent: i }).unwrap();    }    on_event.send(DownloadEvent::Complete { path: "/downloads/file".into() }).unwrap();}``` ```typescriptimport { invoke, Channel } from '@tauri-apps/api/core'; const channel = new Channel<DownloadEvent>();channel.onmessage = (msg) => console.log(msg.event, msg.data);await invoke('download', { url: 'https://...', onEvent: channel });``` ### Window Access Pattern Tauri v2 uses `WebviewWindow` for unified window and webview management. ```rustuse tauri::Manager; #[tauri::command]fn focus_window(app: tauri::AppHandle) {    if let Some(window) = app.get_webview_window("main") {        let _ = window.set_focus();    }}``` **Why this matters:** Use `tauri::WebviewWindow` and `app.get_webview_window("label")` in v2 — the v1 `app.get_window()` API is removed in v2. ## Bundled Resources ### References Located in `references/`:- [`capabilities-reference.md`](references/capabilities-reference.md) - Permission patterns and examples- [`ipc-patterns.md`](references/ipc-patterns.md) - Complete IPC examples- [`plugin-reference.md`](references/plugin-reference.md) - Official plugin install, registration, and permission strings- [`updater-distribution-reference.md`](references/updater-distribution-reference.md) - Signing, HTTPS requirements, and bundle shipping- [`advanced-runtime-reference.md`](references/advanced-runtime-reference.md) - `TrayIconBuilder`, sidecars, deep links, and asset protocols > **Note:** For deep dives on specific topics, see the reference files above. ## Dependencies ### Required | Package | Version | Purpose ||---------|---------|---------|| `@tauri-apps/cli` | ^2 (v2+) | CLI tooling || `@tauri-apps/api` | ^2 (v2+) | Frontend APIs || `tauri` | ^2 (v2+) | Rust core || `tauri-build` | ^2 (v2+) | Build scripts | *\*Last verified: 2026-04-02. Always check [official changelog](https://github.com/tauri-apps/tauri/blob/dev/crates/tauri/CHANGELOG.md) for feature timing.* ### Optional (Plugins) | Package | Version | Purpose | Key Permission ||---------|---------|---------|----------------|| `tauri-plugin-fs` | ^2 (v2+) | File system access | `fs:default` || `tauri-plugin-dialog` | ^2 (v2+) | Native dialogs | `dialog:default` || `tauri-plugin-shell` | ^2 (v2+) | Shell commands, open URLs | `shell:default` || `tauri-plugin-http` | ^2 (v2+) | HTTP client | `http:default` || `tauri-plugin-store` | ^2 (v2+) | Key-value storage | `store:default` | > **Plugin permissions are mandatory.** Installing a plugin without adding its permission string to a capability file causes silent runtime failures. See [`references/plugin-reference.md`](references/plugin-reference.md) for full install + permission details for all official plugins. ## Official Documentation - [Tauri v2+ Documentation](https://v2.tauri.app/)- [Commands Reference](https://v2.tauri.app/develop/calling-rust/)- [Capabilities & Permissions](https://v2.tauri.app/security/capabilities/)- [Configuration Reference](https://v2.tauri.app/reference/config/) ## Troubleshooting ### White Screen on Launch **Symptoms:** App launches but shows blank white screen **Solution:**1. Verify `devUrl` matches your frontend dev server port2. Check `beforeDevCommand` runs your dev server3. Open DevTools (Cmd+Option+I / Ctrl+Shift+I) to check for errors ### Command Returns Undefined **Symptoms:** `invoke()` returns undefined instead of expected value **Solution:**1. Verify command is in `generate_handler![]`2. Check Rust command actually returns a value3. Ensure argument names match (camelCase in JS, snake_case in Rust by default) ### Mobile Build Failures **Symptoms:** Android/iOS build fails with missing target **Solution:**```bash# Android targetsrustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android # iOS targets (macOS only)rustup target add aarch64-apple-ios x86_64-apple-ios aarch64-apple-ios-sim``` ### Desktop vs Mobile Behavioral Differences Not all Tauri APIs and plugins support mobile (iOS/Android). Before using any plugin or API in a mobile build: 1. **Check the plugin page** at `v2.tauri.app/plugin/<name>/` for platform support matrix2. **Common desktop-only items**: System tray (`TrayIconBuilder`), window labels/multi-window, some shell plugin features3. **Mobile-safe patterns**: IPC commands/events/channels work on all platforms; `tauri::AppHandle` is mobile-safe4. **Conditional compilation**: Use `#[cfg(desktop)]` / `#[cfg(mobile)]` for platform-specific Rust logic ```rust#[tauri::command]fn platform_info() -> String {    #[cfg(desktop)]    return "desktop".to_string();    #[cfg(mobile)]    return "mobile".to_string();}``` ## Setup Checklist Before using this skill, verify: - [ ] `npx tauri info` shows correct Tauri v2 versions- [ ] `src-tauri/capabilities/default.json` exists with at least `core:default`- [ ] All commands registered in `generate_handler![]`- [ ] `lib.rs` contains shared code (for mobile support)- [ ] Required Rust targets installed for target platforms