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
-
Permission denied errors from missing capabilities
-
IPC failures from unregistered commands in generate_handler!
-
State management panics from type mismatches
-
Mobile build failures from missing Rust targets
-
White screen issues from misconfigured dev URLs
Quick Start
Step 1: Create a Tauri Command
// src-tauri/src/lib.rs #[tauri::command] fn greet(name: String) -> String { format!("Hello, {}!", name) }
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.
Step 2: Call from Frontend
import { 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
// 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)
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:
#[tauri::command] async fn bad(name: &str) -> String { // Compile error! name.to_string() }
Correct - Owned type:
#[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
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>
Configuration Reference
tauri.conf.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
Cargo.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 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()) }
State Management Pattern
use 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
use 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(); }); }
import { 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
use 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(); }
import { 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 });
Bundled Resources
References
Located in references/ :
-
capabilities-reference.md
-
Permission patterns and examples
-
ipc-patterns.md
-
Complete IPC examples
Note: For deep dives on specific topics, see the reference files above.
Dependencies
Required
Package Version Purpose
@tauri-apps/cli
^2.0.0 CLI tooling
@tauri-apps/api
^2.0.0 Frontend APIs
tauri
^2.0.0 Rust core
tauri-build
^2.0.0 Build scripts
Optional (Plugins)
Package Version Purpose
tauri-plugin-fs
^2.0.0 File system access
tauri-plugin-dialog
^2.0.0 Native dialogs
tauri-plugin-shell
^2.0.0 Shell commands, open URLs
tauri-plugin-http
^2.0.0 HTTP client
tauri-plugin-store
^2.0.0 Key-value storage
Official Documentation
-
Tauri v2 Documentation
-
Commands Reference
-
Capabilities & Permissions
-
Configuration Reference
Troubleshooting
White Screen on Launch
Symptoms: App launches but shows blank white screen
Solution:
-
Verify devUrl matches your frontend dev server port
-
Check beforeDevCommand runs your dev server
-
Open DevTools (Cmd+Option+I / Ctrl+Shift+I) to check for errors
Command Returns Undefined
Symptoms: invoke() returns undefined instead of expected value
Solution:
-
Verify command is in generate_handler![]
-
Check Rust command actually returns a value
-
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:
Android targets
rustup 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
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