tauri-v2

Tauri v2+ cross-platform app development with Rust backend. Use when configuring tauri.conf.json, implementing Rust commands (

Skill file

Preview skill file
---
name: tauri-v2
description: "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 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 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.

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

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

Events are fire-and-forget notifications. See [`references/ipc-patterns.md`](references/ipc-patterns.md) for bidirectional examples.

```rust
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();
    });
}
```

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

Channels provide high-frequency, typed streaming from Rust to Frontend. See [`references/ipc-patterns.md`](references/ipc-patterns.md) for full implementation details.

```rust
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();
}
```

```typescript
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 });
```

### Window Access Pattern

Tauri v2 uses `WebviewWindow` for unified window and webview management.

```rust
use 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 port
2. Check `beforeDevCommand` runs your dev server
3. 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 value
3. 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 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
```

### 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 matrix
2. **Common desktop-only items**: System tray (`TrayIconBuilder`), window labels/multi-window, some shell plugin features
3. **Mobile-safe patterns**: IPC commands/events/channels work on all platforms; `tauri::AppHandle` is mobile-safe
4. **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

Source

Creator's repository · nodnarbnitram/claude-code-extensions

View on GitHub

Security

Security checks in progress
Results will appear here once audits complete
What this skill can do
Reads your filesConnects to the internetRuns code on your machine
Checked by 3 independent security firms
Does it try to trick the AI?Not yet checkedPending · Gen Agent Trust Hub
Does it sneak in hidden code?Not yet checkedPending · Socket
Does it have known bugs?Not yet checkedPending · Snyk