Claude
Skills
Sign in
Back

tauri-v2

Included with Lifetime
$97 forever

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.

Backend & APIs

What this skill does


# 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 capabilities
2. IPC failures from unregistered commands in `generate_handler!`
3. State management panics from type mismatches
4. Mobile build failures from missing Rust targets
5. 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

```typescript
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

```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.

```rust
use thiserror::Error;

#[derive(Debug, Error)]
enum AppError {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
    #[error("Not fou

Related in Backend & APIs