README.md
# Tauri v2+ Development Skill
> Build cross-platform desktop and mobile apps with web frontends and Rust backends.
| | |
|---|---|
| **Status** | Active |
| **Version** | 1.0.1 |
| **Last Updated** | 2026-04-02 |
| **Confidence** | 4/5 |
| **Production Tested** | https://v2.tauri.app/ |
## What This Skill Does
Provides expert assistance for Tauri v2 application development, covering the full development lifecycle from project setup to cross-platform deployment. Specializes in Rust backend commands, IPC patterns, security configuration, and frontend-backend communication.
### Core Capabilities
- Implement Rust commands with `#[tauri::command]` and proper error handling
- Configure IPC patterns (invoke, events, channels) for frontend-backend communication
- Set up security capabilities and permissions for plugins and APIs
- Access exhaustive reference docs for plugins (fs, dialog, shell, store, etc.), updater/distribution signing, and advanced runtime (tray, sidecars, deep links)
- Build and deploy for desktop (macOS, Windows, Linux) and mobile (iOS, Android)
- Integrate Vite + TanStack Router frontends with Tauri backends
- Configure tauri.conf.json and Cargo.toml for cross-platform builds
## Auto-Trigger Keywords
### Primary Keywords
Exact terms that strongly trigger this skill:
- tauri
- tauri v2
- tauri.conf.json
- src-tauri
- #[tauri::command]
- tauri::invoke
- capabilities.json
### Secondary Keywords
Related terms that may trigger in combination:
- rust backend
- desktop app
- cross-platform app
- webview
- invoke command
- emit event
- app permissions
- bundle desktop
### Error-Based Keywords
Common error messages that should trigger this skill:
- "Command not found"
- "Permission denied" (in Tauri context)
- "Failed to invoke command"
- "Missing capability"
- "Cannot read property of undefined" (invoke result)
- "tauri build failed"
- "Missing Rust target"
## Known Issues Prevention
| Issue | Root Cause | Solution |
|-------|-----------|----------|
| Command not found | Missing from `generate_handler![]` | Register all commands in the macro |
| Permission denied | Missing capability configuration | Add required permissions to capabilities file |
| State access panic | Type mismatch in `State<T>` | Use exact type matching `.manage()` call |
| White screen | Frontend not building | Verify `beforeDevCommand` and `devUrl` |
| Mobile build fails | Missing Rust targets | Run `rustup target add <platform-targets>` |
| IPC timeout | Blocking in async command | Use non-blocking async or spawn threads |
## When to Use
### Use This Skill For
- Creating new Tauri v2 projects or commands
- Configuring permissions and capabilities
- Setting up IPC (invoke, events, channels)
- Debugging command invocation issues
- Cross-platform build configuration
- Plugin integration and configuration
- Mobile (iOS/Android) deployment setup
### Don't Use This Skill For
- Tauri v1 development (use migration guide then this skill)
- Pure frontend development without Tauri integration
- Native mobile development (Swift/Kotlin directly)
- Backend API development without Tauri
## Version Policy
> [!NOTE]
> This skill targets **Tauri v2+**. Feature availability may vary across minor versions. When exact version timing matters, check the [official Tauri changelog](https://github.com/tauri-apps/tauri/blob/dev/crates/tauri/CHANGELOG.md) and release notes for `tauri`, `@tauri-apps/api`, `@tauri-apps/cli`, and relevant plugins.
## Quick Usage
```bash
# Create new Tauri project
npm create tauri-app@latest
# Add Tauri to existing project
npm install -D @tauri-apps/cli@latest
npx tauri init
# Development
npm run tauri dev
# Production build
npm run tauri build
# Add a plugin (e.g., fs, dialog, store)
cargo tauri add fs # Adds tauri-plugin-fs to Cargo.toml + JS package
cargo tauri add dialog
cargo tauri add store
# Mobile development
cargo tauri android init # One-time setup
cargo tauri android dev # Run on Android
cargo tauri android build # Release build
cargo tauri ios init # One-time setup (macOS only)
cargo tauri ios dev # Run on iOS simulator
cargo tauri ios build # Release build
```
## Token Efficiency
| Approach | Estimated Tokens | Time |
|----------|-----------------|------|
| Manual Implementation | ~15,000 | 2+ hours |
| With This Skill | ~6,000 | 30 min |
| **Savings** | **60%** | **~1.5 hours** |
## Reference Documentation
For deep-dive guidance on specific topics, see the following reference files:
| Topic | Reference File | Purpose |
|-------|----------------|---------|
| **Security & Permissions** | [`capabilities-reference.md`](references/capabilities-reference.md) | V2 security model, capability files, permissions, and scopes |
| **IPC Patterns** | [`ipc-patterns.md`](references/ipc-patterns.md) | Decision framework for Commands, Events, and Channels |
| **Official Plugins** | [`plugin-reference.md`](references/plugin-reference.md) | Install, registration, and permissions for all official plugins |
| **Updater & Distribution** | [`updater-distribution-reference.md`](references/updater-distribution-reference.md) | Signing, updater setup, and platform-specific distribution |
| **Advanced Runtime** | [`advanced-runtime-reference.md`](references/advanced-runtime-reference.md) | Tray icons, sidecars, deep links, and custom protocols |
See the [References Index](references/README.md) for a complete navigation guide.
## File Structure
```
tauri-v2/
├── SKILL.md # Quick-start patterns, core rules, critical guidance
├── README.md # This file - discovery and quick reference
└── references/ # Deep-dive reference documentation
├── README.md # Index and navigation
├── capabilities-reference.md # Security model, permissions, scopes
├── ipc-patterns.md # Commands, events, channels decision framework
├── plugin-reference.md # Exhaustive plugin install/register/permissions
├── updater-distribution-reference.md # Signing, HTTPS, platform distribution
└── advanced-runtime-reference.md # Tray, sidecars, deep links, protocols
```
## Dependencies
| Package | Version | Verified |
|---------|---------|----------|
| `@tauri-apps/cli` | ^2 (v2+) | 2026-04-02* |
| `@tauri-apps/api` | ^2 (v2+) | 2026-04-02* |
| `tauri` (Rust) | ^2 (v2+) | 2026-04-02* |
| `tauri-build` (Rust) | ^2 (v2+) | 2026-04-02* |
*\*Last verified: 2026-04-02. Always check [official changelog](https://github.com/tauri-apps/tauri/blob/dev/crates/tauri/CHANGELOG.md) for feature timing.*
## Official Documentation
- [Tauri v2+ Documentation](https://v2.tauri.app/)
- [Commands Reference](https://v2.tauri.app/develop/calling-rust/)
- [IPC Concepts](https://v2.tauri.app/concept/inter-process-communication/)
- [Capabilities & Permissions](https://v2.tauri.app/security/capabilities/)
- [Configuration Reference](https://v2.tauri.app/reference/config/)
- [Plugin Directory](https://v2.tauri.app/plugin/)
## Related Skills
- `tanstack-start-expert` - TanStack Router patterns for type-safe frontend routing
- `react-component-architect` - React component patterns for Tauri frontends
- `go-google-style-expert` - Alternative backend patterns (if using Go instead)
## Companion Agent (Deprecated)
The `tauri-v2-expert` agent at `.claude/agents/specialized/tauri/tauri-v2-expert.md` is **deprecated/legacy**. This skill is the preferred and actively maintained interface. Use this skill over the agent for all new Tauri v2 work.
---
**License:** MIT
references/advanced-runtime-reference.md
# Tauri v2+ Advanced Runtime Reference
## Contents
- System Tray (`TrayIconBuilder`)
- Sidecars (External Binaries)
- Deep Links (`tauri-plugin-deep-link`)
- Custom Protocols
> Covers system tray integration, sidecar processes, deep links, and custom protocols.
> *Last verified: 2026-04-02. Check official Tauri v2+ docs for updates.*
**See also:**
- [plugin-reference.md](plugin-reference.md) — plugin installation and permissions
- [capabilities-reference.md](capabilities-reference.md) — capability/permission model
## Section 1: System Tray (`TrayIconBuilder`)
> **v2 Change:** `SystemTray` from v1 is replaced by `TrayIconBuilder` in v2. Do NOT use `SystemTray`.
```rust
// In lib.rs run() function, in setup hook:
use tauri::{
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
Manager,
};
tauri::Builder::default()
.setup(|app| {
let tray = TrayIconBuilder::new()
.icon(app.default_window_icon().unwrap().clone())
.tooltip("My App")
.on_tray_icon_event(|tray, event| {
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
} = event
{
let app = tray.app_handle();
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
})
.build(app)?;
Ok(())
})
```
Show tray menu example:
```rust
use tauri::menu::{Menu, MenuItem};
let quit_item = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&quit_item])?;
let tray = TrayIconBuilder::new()
.icon(app.default_window_icon().unwrap().clone())
.menu(&menu)
.on_menu_event(|app, event| match event.id.as_ref() {
"quit" => app.exit(0),
_ => {}
})
.build(app)?;
```
Platform notes:
- **macOS:** tray icon appears in menu bar; supports template images
- **Windows:** tray icon in system tray; click events differ from macOS
- **Linux:** tray support varies by desktop environment (requires `libappindicator` or `libayatana-appindicator`)
## Section 2: Sidecars (External Binaries)
Show config and usage for bundled executables:
```json
// tauri.conf.json
{
"bundle": {
"externalBin": [
"binaries/my-sidecar"
]
}
}
```
Capability permission required:
```json
{
"permissions": [
{
"identifier": "shell:allow-execute",
"allow": [
{ "name": "my-sidecar", "args": true, "sidecar": true }
]
}
]
}
```
Rust code to execute sidecar:
```rust
use tauri_plugin_shell::ShellExt;
#[tauri::command]
async fn run_sidecar(app: tauri::AppHandle) -> Result<String, String> {
let output = app.shell()
.sidecar("my-sidecar")
.map_err(|e| e.to_string())?
.args(["--flag", "value"])
.output()
.await
.map_err(|e| e.to_string())?;
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
```
Binary naming convention (for cross-platform bundling):
- **macOS (Intel):** `my-sidecar-x86_64-apple-darwin`
- **macOS (ARM):** `my-sidecar-aarch64-apple-darwin`
- **Windows:** `my-sidecar-x86_64-pc-windows-msvc.exe`
- **Linux:** `my-sidecar-x86_64-unknown-linux-gnu`
## Section 3: Deep Links (`tauri-plugin-deep-link`)
```bash
cargo tauri add deep-link
```
Config in tauri.conf.json:
```json
{
"plugins": {
"deep-link": {
"mobile": [
{ "scheme": "myapp" }
],
"desktop": [
{ "schemes": ["myapp"] }
]
}
}
}
```
Capability:
```json
{ "permissions": ["deep-link:default"] }
```
Handling deep links in Rust:
```rust
use tauri_plugin_deep_link::DeepLinkExt;
app.deep_link().on_open_url(|event| {
println!("Deep link: {:?}", event.urls());
});
```
Platform notes:
- **macOS:** registers URL scheme in Info.plist automatically
- **Windows:** registry entry created during install
- **Linux:** .desktop file update required
- **iOS/Android:** configure in respective platform files (AndroidManifest.xml or Info.plist)
## Section 4: Custom Protocols
> **Scope note:** Custom protocol (`tauri://` and custom schemes via `invoke_filter` or `asset_protocol`) is a more advanced feature. The primary official pattern is the built-in `asset` protocol for serving local files. Custom protocol handlers require careful security consideration.
Show asset protocol access (most common use case):
```json
// tauri.conf.json
{
"app": {
"security": {
"assetScope": ["$APPDATA/assets/**", "$RESOURCE/**"]
}
}
}
```
```typescript
// Access local file via asset protocol
const imgSrc = convertFileSrc('/path/to/image.png');
```
Note: Full custom protocol registration (`tauri::Builder::register_uri_scheme_protocol`) is available but underdocumented in official v2+ docs as of 2026-04-02. Prefer asset protocol for local file serving.
references/capabilities-reference.md
# Tauri v2+ Capabilities & Permissions Reference
## Contents
- Security Model: v1 vs v2
- Overview
- Capability File Structure
- Core Permissions
- Plugin Permissions
- Scopes
- Permission Sets
- Window and Webview Targeting
- Capability Best Practices
- Common Capability Patterns
- Anti-Patterns
## Security Model: v1 vs v2
Tauri v2 replaces the v1 `allowlist` with a capabilities-first security model. In v1, you listed allowed API calls in `tauri.conf.json`'s `allowlist`. In v2, permissions must be explicitly granted via capability files in `src-tauri/capabilities/`.
**Three-layer security model:**
- **Capability**: A named collection of permissions, scoped to specific windows/webviews. Lives in `src-tauri/capabilities/*.json`.
- **Permission**: An identifier that grants access to a specific command or feature (e.g., `fs:allow-read-file`). Defined per-plugin.
- **Scope**: Optional constraint on a permission that limits what it can access (e.g., only `$APPDATA/*` paths). Part of a permission object.
## Overview
Tauri v2+ uses a capabilities-based security model. By default, **nothing is allowed** - you must explicitly grant permissions through capability files.
*Last verified: 2026-04-02. Check the official Tauri changelog when capability semantics or permission names change.*
## Capability File Structure
Location: `src-tauri/capabilities/`
```json
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "capability-name",
"description": "What this capability allows",
"windows": ["main", "settings"],
"webviews": [],
"permissions": [
"core:default",
"plugin-name:permission-name"
]
}
```
## Core Permissions
### Essential (Almost Always Needed)
```json
{
"permissions": [
"core:default",
"core:window:default",
"core:event:default"
]
}
```
### Window Permissions
| Permission | Description |
|------------|-------------|
| `core:window:default` | Basic window operations |
| `core:window:allow-close` | Allow closing windows |
| `core:window:allow-set-title` | Allow changing window title |
| `core:window:allow-minimize` | Allow minimizing |
| `core:window:allow-maximize` | Allow maximizing |
| `core:window:allow-set-size` | Allow resizing |
| `core:window:allow-set-position` | Allow repositioning |
| `core:window:allow-set-fullscreen` | Allow fullscreen toggle |
### Event Permissions
| Permission | Description |
|------------|-------------|
| `core:event:default` | Basic event listening |
| `core:event:allow-emit` | Allow emitting events |
| `core:event:allow-listen` | Allow listening to events |
## Plugin Permissions
### File System (`tauri-plugin-fs`)
```json
{
"permissions": [
"fs:default",
"fs:allow-read-dir",
"fs:allow-read-file",
"fs:allow-write-file",
"fs:allow-create-dir",
"fs:allow-remove-file",
"fs:allow-rename"
]
}
```
**With Scopes:**
```json
{
"permissions": [
{
"identifier": "fs:allow-read-file",
"allow": [
{ "path": "$APPDATA/*" },
{ "path": "$HOME/Documents/*" }
]
}
]
}
```
### Dialog (`tauri-plugin-dialog`)
```json
{
"permissions": [
"dialog:default",
"dialog:allow-open",
"dialog:allow-save",
"dialog:allow-message",
"dialog:allow-ask",
"dialog:allow-confirm"
]
}
```
### Shell (`tauri-plugin-shell`)
```json
{
"permissions": [
"shell:default",
"shell:allow-open",
"shell:allow-execute"
]
}
```
**Scoped Execute:**
```json
{
"permissions": [
{
"identifier": "shell:allow-execute",
"allow": [
{ "name": "git", "args": true },
{ "name": "npm", "args": ["install", "run"] }
]
}
]
}
```
### HTTP (`tauri-plugin-http`)
```json
{
"permissions": [
"http:default"
]
}
```
**With URL Scopes:**
```json
{
"permissions": [
{
"identifier": "http:default",
"allow": [
{ "url": "https://api.example.com/*" },
{ "url": "https://*.myapp.com/*" }
]
}
]
}
```
### Store (`tauri-plugin-store`)
```json
{
"permissions": [
"store:default",
"store:allow-get",
"store:allow-set",
"store:allow-delete",
"store:allow-keys",
"store:allow-clear"
]
}
```
### Clipboard (`tauri-plugin-clipboard-manager`)
```json
{
"permissions": [
"clipboard-manager:default",
"clipboard-manager:allow-read",
"clipboard-manager:allow-write"
]
}
```
### Notification (`tauri-plugin-notification`)
```json
{
"permissions": [
"notification:default",
"notification:allow-send",
"notification:allow-request-permission"
]
}
```
### Global Shortcut (`tauri-plugin-global-shortcut`)
```json
{
"permissions": [
"global-shortcut:default",
"global-shortcut:allow-register",
"global-shortcut:allow-unregister"
]
}
```
## Permission Sets
Permission sets allow grouping multiple permissions into a single reusable identifier. You can use preset permission sets provided by plugins (like `fs:default`) or define your own in `src-tauri/permissions/`.
```json
{
"permissions": [
"fs:default", // Permission set: includes common fs operations
"fs:allow-read-file", // Individual permission: specific operation
{
"identifier": "fs:allow-read-file", // Permission with scope
"allow": [{ "path": "$APPDATA/*" }]
}
]
}
```
## Platform-Specific Capabilities
```json
{
"identifier": "desktop-only",
"platforms": ["linux", "macos", "windows"],
"permissions": ["global-shortcut:default"]
}
```
```json
{
"identifier": "mobile-only",
"platforms": ["iOS", "android"],
"permissions": ["biometric:default", "haptics:default"]
}
```
## Windows and Webviews Targeting
Capabilities are applied to specific windows and webviews by their labels. A window or webview can be part of multiple capabilities, in which case their permissions are merged.
```json
{
"identifier": "main-window-cap",
"windows": ["main"], // Target by window label
"webviews": [], // Or target specific webviews
"permissions": ["core:default", "fs:default"]
}
```
## Remote URL Access
Allow Tauri commands from remote URLs:
```json
{
"identifier": "remote-access",
"remote": {
"urls": ["https://*.myapp.com"]
},
"permissions": ["http:default"]
}
```
## Custom Permission Files
Create custom permissions in `src-tauri/permissions/`:
**`custom.toml`:**
```toml
[[permission]]
identifier = "allow-home-documents"
description = "Allow access to home documents"
commands.allow = ["read_file", "write_file"]
[[scope.allow]]
path = "$HOME/Documents/**"
```
Reference in capability:
```json
{
"permissions": ["custom:allow-home-documents"]
}
```
## Capability Best Practices
1. **Principle of Least Privilege**: Only grant what's needed
2. **Use Scopes**: Limit file/URL access to specific paths
3. **Separate Capabilities**: Create focused capability files for different features
4. **Platform-Specific**: Use platform filtering for platform-specific features
5. **Document**: Add descriptions to explain why permissions are needed
**See also:** [Plugin Reference](plugin-reference.md) for plugin-specific permission strings | [Advanced Runtime](advanced-runtime-reference.md) for tray/sidecar capabilities
## Anti-Pattern: Missing Capability
Plugin installed but **NOT** in capabilities = silent permission denied at runtime. Always add plugin permissions to a capability file that targets the window using the plugin.
## Common Capability Patterns
### Minimal App
```json
{
"identifier": "minimal",
"windows": ["main"],
"permissions": ["core:default"]
}
```
### File Manager
```json
{
"identifier": "file-manager",
"windows": ["main"],
"permissions": [
"core:default",
"fs:default",
"dialog:allow-open",
"dialog:allow-save"
]
}
```
### Web-Connected App
```json
{
"identifier": "web-app",
"windows": ["main"],
"permissions": [
"core:default",
"http:default",
"shell:allow-open"
]
}
```
### Full Desktop App
```json
{
"identifier": "full-desktop",
"windows": ["main"],
"permissions": [
"core:default",
"core:window:default",
"core:event:default",
"fs:default",
"dialog:default",
"shell:default",
"clipboard-manager:default",
"notification:default",
"global-shortcut:default",
"store:default"
]
}
```
references/ipc-patterns.md
# Tauri v2+ IPC Patterns Reference
## Contents
- Overview
- IPC Decision Framework
- Commands (invoke)
- Events
- Typed Streaming Channels
- State Management
- Error Handling Across IPC
- Window Access and App Handles
- IPC Selection Guide
## Overview
Tauri v2+ provides three IPC primitives:
1. **Commands**: Request-response (most common)
2. **Events**: Fire-and-forget notifications
3. **Channels**: High-frequency streaming
**See also:** [Capabilities Reference](capabilities-reference.md) for permission setup | [Plugin Reference](plugin-reference.md) for plugin-specific IPC
*Last verified: 2026-04-02. Check the official Tauri changelog when IPC API timing matters.*
## IPC Decision Framework
### Commands: Request-Response
Use `invoke()` when:
- Frontend needs data from Rust (fetch, compute, query)
- Frontend triggers an action and needs a result
- Error handling is needed (returns `Result<T, E>`)
- **Direction: Frontend → Rust → Frontend** (request/response)
### Events: Fire-and-Forget Notifications
Use `emit()`/`listen()` when:
- Rust needs to notify frontend of a background event
- Multiple windows need to receive the same notification
- Broadcasting state changes that don't require acknowledgment
- **Direction: Bidirectional** (but one-way per emit)
- **Important:** Events are fire-and-forget — there is NO acknowledgment or response channel
### Channels: Typed Streaming
Use `Channel<T>` when:
- High-frequency progress updates from a long-running operation
- Streaming data from Rust to frontend
- Strongly typed discriminated message streams
- **Direction: Rust → Frontend** (streaming only)
- **Key difference from Events:** Channels are scoped to a single command invocation; events are global
## Commands (invoke)
### Basic Command
**Rust:**
```rust
#[tauri::command]
fn greet(name: String) -> String {
format!("Hello, {}!", name)
}
// Register in builder
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet])
```
**Frontend:**
```typescript
import { invoke } from '@tauri-apps/api/core';
const result = await invoke<string>('greet', { name: 'World' });
```
### Command with Multiple Arguments
**Rust:**
```rust
#[tauri::command]
fn calculate(a: i32, b: i32, operation: String) -> i32 {
match operation.as_str() {
"add" => a + b,
"sub" => a - b,
"mul" => a * b,
"div" => a / b,
_ => 0,
}
}
```
**Frontend:**
```typescript
const result = await invoke<number>('calculate', {
a: 10,
b: 5,
operation: 'add'
});
```
### Async Command
**Rust:**
```rust
#[tauri::command]
async fn fetch_data(url: String) -> Result<String, String> {
// Use owned types (String, not &str) in async commands
let response = reqwest::get(&url)
.await
.map_err(|e| e.to_string())?;
response.text()
.await
.map_err(|e| e.to_string())
}
```
**Frontend:**
```typescript
try {
const data = await invoke<string>('fetch_data', { url: 'https://api.example.com' });
} catch (error) {
console.error('Failed:', error);
}
```
### Command with Result Error Handling
**Rust:**
```rust
use thiserror::Error;
#[derive(Debug, Error)]
enum AppError {
#[error("File not found: {0}")]
NotFound(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Permission denied")]
PermissionDenied,
}
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 read_config(path: String) -> Result<Config, AppError> {
if !std::path::Path::new(&path).exists() {
return Err(AppError::NotFound(path));
}
// ...
}
```
**Frontend:**
```typescript
try {
const config = await invoke<Config>('read_config', { path: '/config.json' });
} catch (error) {
// error is the serialized error string
console.error('Config error:', error);
}
```
### Command with State
**Rust:**
```rust
use std::sync::Mutex;
use tauri::State;
struct AppState {
counter: u32,
items: Vec<String>,
}
#[tauri::command]
fn get_count(state: State<'_, Mutex<AppState>>) -> u32 {
state.lock().unwrap().counter
}
#[tauri::command]
fn increment(state: State<'_, Mutex<AppState>>) -> u32 {
let mut s = state.lock().unwrap();
s.counter += 1;
s.counter
}
#[tauri::command]
fn add_item(item: String, state: State<'_, Mutex<AppState>>) {
state.lock().unwrap().items.push(item);
}
// In builder:
tauri::Builder::default()
.manage(Mutex::new(AppState { counter: 0, items: vec![] }))
.invoke_handler(tauri::generate_handler![get_count, increment, add_item])
```
### Command with Window Access
**Rust:**
```rust
use tauri::{WebviewWindow, AppHandle};
#[tauri::command]
fn get_window_info(window: WebviewWindow) -> String {
format!("Window label: {}", window.label())
}
#[tauri::command]
fn create_window(app: AppHandle) -> Result<(), String> {
tauri::WebviewWindowBuilder::new(
&app,
"new-window",
tauri::WebviewUrl::App("index.html".into())
)
.title("New Window")
.build()
.map_err(|e| e.to_string())?;
Ok(())
}
```
### Command with Raw Binary Data
**Rust:**
```rust
use tauri::ipc::Response;
#[tauri::command]
fn read_binary_file(path: String) -> Result<Response, String> {
let data = std::fs::read(&path).map_err(|e| e.to_string())?;
Ok(Response::new(data)) // Avoids JSON serialization overhead
}
#[tauri::command]
fn upload_file(request: tauri::ipc::Request) -> Result<(), String> {
let tauri::ipc::InvokeBody::Raw(data) = request.body() else {
return Err("Expected raw body".into());
};
std::fs::write("upload.bin", data).map_err(|e| e.to_string())
}
```
**Frontend:**
```typescript
// Reading binary
const data = await invoke<ArrayBuffer>('read_binary_file', { path: '/file.bin' });
// Uploading binary
const fileData = new Uint8Array([1, 2, 3, 4]);
await invoke('upload_file', fileData);
```
---
## Events
> **Trait imports required:** `use tauri::Emitter;` to call `.emit()` on `AppHandle`/`WebviewWindow`. `use tauri::Listener;` to call `.listen()` on `App`/`AppHandle`. These traits must be in scope.
### Emit from Rust to Frontend
**Rust:**
```rust
use tauri::Emitter;
#[tauri::command]
fn start_background_task(app: tauri::AppHandle) {
std::thread::spawn(move || {
for i in 0..100 {
std::thread::sleep(std::time::Duration::from_millis(100));
app.emit("progress", i).unwrap();
}
app.emit("complete", "Task finished").unwrap();
});
}
// Emit to specific window
#[tauri::command]
fn notify_window(app: tauri::AppHandle, window_label: String, message: String) {
app.emit_to(&window_label, "notification", message).unwrap();
}
```
**Frontend:**
```typescript
import { listen, once } from '@tauri-apps/api/event';
// Listen continuously
const unlisten = await listen<number>('progress', (event) => {
console.log(`Progress: ${event.payload}%`);
});
// Listen once
await once<string>('complete', (event) => {
console.log(event.payload);
});
// Clean up when done
unlisten();
```
### Emit from Frontend to Rust
**Frontend:**
```typescript
import { emit } from '@tauri-apps/api/event';
await emit('user-action', { action: 'click', target: 'button' });
```
**Rust (in setup or command):**
```rust
use tauri::Listener;
fn setup_listeners(app: &tauri::App) {
app.listen("user-action", |event| {
println!("User action: {:?}", event.payload());
});
}
```
### Window-Specific Events
**Rust:**
```rust
use tauri::{Emitter, WebviewWindow};
#[tauri::command]
fn emit_to_window(window: WebviewWindow, message: String) {
window.emit("window-message", message).unwrap();
}
```
---
## Typed Streaming Channels
`Channel<TSend>` is a typed streaming primitive. The type parameter `TSend` defines what messages can be sent. Both Rust and TypeScript must agree on the shape:
- Rust: `Channel<MyEvent>` where `MyEvent: serde::Serialize + Clone`
- Frontend: `new Channel<MyEvent>()` with matching TypeScript type
- Use `#[serde(tag = "event", content = "data")]` on enums for discriminated union patterns.
**Rust:**
```rust
use tauri::ipc::Channel;
#[derive(Clone, serde::Serialize)]
struct ProgressUpdate {
current: u32,
total: u32,
message: String,
}
#[tauri::command]
async fn process_files(
files: Vec<String>,
on_progress: Channel<ProgressUpdate>
) -> Result<(), String> {
let total = files.len() as u32;
for (i, file) in files.iter().enumerate() {
// Process file...
on_progress.send(ProgressUpdate {
current: i as u32 + 1,
total,
message: format!("Processing {}", file),
}).unwrap();
}
Ok(())
}
```
**Frontend:**
```typescript
import { invoke, Channel } from '@tauri-apps/api/core';
interface ProgressUpdate {
current: number;
total: number;
message: string;
}
const channel = new Channel<ProgressUpdate>();
channel.onmessage = (update) => {
const percent = (update.current / update.total) * 100;
console.log(`${percent}% - ${update.message}`);
};
await invoke('process_files', {
files: ['file1.txt', 'file2.txt'],
onProgress: channel
});
```
### Tagged Union Events (Discriminated)
**Rust:**
```rust
use tauri::ipc::Channel;
#[derive(Clone, serde::Serialize)]
#[serde(tag = "event", content = "data")]
enum DownloadEvent {
Started { url: String, size: u64 },
Progress { downloaded: u64, total: u64 },
Complete { path: String },
Error { message: String },
}
#[tauri::command]
async fn download_file(
url: String,
on_event: Channel<DownloadEvent>
) -> Result<String, String> {
on_event.send(DownloadEvent::Started {
url: url.clone(),
size: 1000,
}).unwrap();
for i in 0..=100 {
on_event.send(DownloadEvent::Progress {
downloaded: i * 10,
total: 1000,
}).unwrap();
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
let path = "/downloads/file.zip".to_string();
on_event.send(DownloadEvent::Complete {
path: path.clone(),
}).unwrap();
Ok(path)
}
```
**Frontend:**
```typescript
import { invoke, Channel } from '@tauri-apps/api/core';
type DownloadEvent =
| { event: 'Started'; data: { url: string; size: number } }
| { event: 'Progress'; data: { downloaded: number; total: number } }
| { event: 'Complete'; data: { path: string } }
| { event: 'Error'; data: { message: string } };
const channel = new Channel<DownloadEvent>();
channel.onmessage = (msg) => {
switch (msg.event) {
case 'Started':
console.log(`Starting download: ${msg.data.url} (${msg.data.size} bytes)`);
break;
case 'Progress':
const percent = (msg.data.downloaded / msg.data.total) * 100;
console.log(`Download: ${percent.toFixed(1)}%`);
break;
case 'Complete':
console.log(`Downloaded to: ${msg.data.path}`);
break;
case 'Error':
console.error(`Download failed: ${msg.data.message}`);
break;
}
};
const path = await invoke<string>('download_file', {
url: 'https://example.com/file.zip',
onEvent: channel
});
```
---
## IPC Selection Guide
| Pattern | Use Case | Direction | Frequency |
|---------|----------|-----------|-----------|
| **Commands** | Request-response, data fetching | Frontend → Rust | One-time |
| **Events** | Notifications, state changes | Bidirectional | Low-medium |
| **Channels** | Progress updates, streaming data | Rust → Frontend | High |
### When to Use Each
**Commands (invoke)**
- Fetching data from Rust
- Performing actions that return results
- CRUD operations
- Most common pattern
**Events (emit/listen)**
- Notifying UI of background changes
- Broadcasting to multiple windows
- Fire-and-forget notifications
- System events (window close, minimize)
**Channels**
- File download/upload progress
- Long-running operations with updates
- Streaming log output
- Real-time data feeds
references/plugin-reference.md
# Tauri v2+ Plugin Reference
## Contents
- General Installation Pattern
- File System
- Dialog
- Shell
- HTTP
- Store
- Clipboard Manager
- Notification
- Global Shortcut
- Updater
- Deep Link
- Opener
- Process
**Important:** Installing a plugin is not enough. Every plugin's permissions must be **explicitly granted** in a capability file under `src-tauri/capabilities/`. Without this, plugin calls will fail silently with permission errors.
*Last verified: 2026-04-02. Check the official plugin changelogs when install flow or permission names change.*
## General Installation Pattern
For most official plugins, the preferred installation method is using the Tauri CLI:
```bash
cargo tauri add <plugin-name>
```
This command automatically:
1. Adds the Rust crate to `src-tauri/Cargo.toml`.
2. Adds the JS/TS package to `package.json` (if applicable).
3. Registers the plugin in `src-tauri/src/lib.rs` (often requiring manual verification).
## 1. File System (`tauri-plugin-fs`)
Access the local file system.
**Install:**
```bash
cargo tauri add fs
```
**Rust registration** (in `src-tauri/src/lib.rs`):
```rust
.plugin(tauri_plugin_fs::init())
```
**JS Package:** `@tauri-apps/plugin-fs`
**Capability permissions** (add to `src-tauri/capabilities/*.json`):
```json
{
"permissions": ["fs:default"]
}
```
**Common permissions:**
- `fs:allow-read-file`: Read file contents.
- `fs:allow-write-file`: Write/create files.
- `fs:allow-read-dir`: List directory contents.
- `fs:allow-exists`: Check if path exists.
**Scopes:**
Path access is restricted by scopes. Common variables: `$APPDATA`, `$HOME`, `$DOCUMENTS`, `$DOWNLOADS`.
**Cross-reference:** See [capabilities-reference.md](capabilities-reference.md) for scope examples.
## 2. Dialog (`tauri-plugin-dialog`)
Native system dialogs for file picking and messages.
**Install:**
```bash
cargo tauri add dialog
```
**Rust registration** (in `src-tauri/src/lib.rs`):
```rust
.plugin(tauri_plugin_dialog::init())
```
**JS Package:** `@tauri-apps/plugin-dialog`
**Capability permissions** (add to `src-tauri/capabilities/*.json`):
```json
{
"permissions": ["dialog:default"]
}
```
**Common permissions:**
- `dialog:allow-open`: Open file/directory picker.
- `dialog:allow-save`: Save file picker.
- `dialog:allow-message`: Show message box.
- `dialog:allow-ask`: Show ask dialog (Yes/No).
## 3. Shell (`tauri-plugin-shell`)
Spawn child processes or open URLs.
**Install:**
```bash
cargo tauri add shell
```
**Rust registration** (in `src-tauri/src/lib.rs`):
```rust
.plugin(tauri_plugin_shell::init())
```
**JS Package:** `@tauri-apps/plugin-shell`
**Capability permissions** (add to `src-tauri/capabilities/*.json`):
```json
{
"permissions": ["shell:default"]
}
```
**Common permissions:**
- `shell:allow-open`: Open URLs in default browser.
- `shell:allow-execute`: Execute arbitrary programs (requires heavy scoping).
**Scoping:**
`allow-execute` requires defining specific programs and allowed arguments in the capability file.
**Cross-reference:** See [capabilities-reference.md](capabilities-reference.md) for shell scope examples.
## 4. HTTP (`tauri-plugin-http`)
Perform HTTP requests from the Rust backend (bypassing CORS).
**Install:**
```bash
cargo tauri add http
```
**Rust registration** (in `src-tauri/src/lib.rs`):
```rust
.plugin(tauri_plugin_http::init())
```
**JS Package:** `@tauri-apps/plugin-http`
**Capability permissions** (add to `src-tauri/capabilities/*.json`):
```json
{
"permissions": ["http:default"]
}
```
**Common permissions:**
- `http:default`: Basic request/response capabilities.
**Scoping:**
Access can be restricted to specific domains or URL patterns.
**Cross-reference:** See [capabilities-reference.md](capabilities-reference.md) for URL scope examples.
## 5. Store (`tauri-plugin-store`)
Simple key-value persistence.
**Install:**
```bash
cargo tauri add store
```
**Rust registration** (in `src-tauri/src/lib.rs`):
```rust
.plugin(tauri_plugin_store::Builder::default().build())
```
**JS Package:** `@tauri-apps/plugin-store`
**Capability permissions** (add to `src-tauri/capabilities/*.json`):
```json
{
"permissions": ["store:default"]
}
```
**Common permissions:**
- `store:allow-get`: Retrieve values.
- `store:allow-set`: Save values.
- `store:allow-load`: Load store from disk.
## 6. Clipboard (`tauri-plugin-clipboard-manager`)
Read and write to the system clipboard.
**Install:**
```bash
cargo tauri add clipboard-manager
```
**Rust registration** (in `src-tauri/src/lib.rs`):
```rust
.plugin(tauri_plugin_clipboard_manager::init())
```
**JS Package:** `@tauri-apps/plugin-clipboard-manager`
**Capability permissions** (add to `src-tauri/capabilities/*.json`):
```json
{
"permissions": ["clipboard-manager:default"]
}
```
**Common permissions:**
- `clipboard-manager:allow-read`: Read clipboard content.
- `clipboard-manager:allow-write`: Write to clipboard.
## 7. Notification (`tauri-plugin-notification`)
Send native desktop notifications.
**Install:**
```bash
cargo tauri add notification
```
**Rust registration** (in `src-tauri/src/lib.rs`):
```rust
.plugin(tauri_plugin_notification::init())
```
**JS Package:** `@tauri-apps/plugin-notification`
**Capability permissions** (add to `src-tauri/capabilities/*.json`):
```json
{
"permissions": ["notification:default"]
}
```
**Common permissions:**
- `notification:allow-send`: Trigger notifications.
- `notification:allow-request-permission`: Check/ask for user permission.
## 8. Global Shortcut (`tauri-plugin-global-shortcut`)
Register system-wide keyboard shortcuts.
**Install:**
```bash
cargo tauri add global-shortcut
```
**Rust registration** (in `src-tauri/src/lib.rs`):
```rust
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
```
**JS Package:** `@tauri-apps/plugin-global-shortcut`
**Capability permissions** (add to `src-tauri/capabilities/*.json`):
```json
{
"permissions": ["global-shortcut:default"]
}
```
**Common permissions:**
- `global-shortcut:allow-register`: Register a new shortcut.
- `global-shortcut:allow-is-registered`: Check if a shortcut is active.
**Note:** Desktop only.
## 9. Updater (`tauri-plugin-updater`)
Automated application updates.
**Install:**
```bash
cargo tauri add updater
```
**Rust registration** (in `src-tauri/src/lib.rs`):
```rust
.plugin(tauri_plugin_updater::Builder::new().build())
```
**JS Package:** `@tauri-apps/plugin-updater`
**Capability permissions** (add to `src-tauri/capabilities/*.json`):
```json
{
"permissions": ["updater:default"]
}
```
**Common permissions:**
- `updater:allow-check`: Check for updates.
- `updater:allow-download-and-install`: Execute update.
**Note:** Requires code signing and an update server/static JSON.
**Cross-reference:** See [updater-distribution-reference.md](updater-distribution-reference.md) for signing requirements.
## 10. Deep Link (`tauri-plugin-deep-link`)
Register and handle custom URL schemes (e.g., `myapp://`).
**Install:**
```bash
cargo tauri add deep-link
```
**Rust registration** (in `src-tauri/src/lib.rs`):
```rust
.plugin(tauri_plugin_deep_link::init())
```
**JS Package:** `@tauri-apps/plugin-deep-link`
**Capability permissions** (add to `src-tauri/capabilities/*.json`):
```json
{
"permissions": ["deep-link:default"]
}
```
**Common permissions:**
- `deep-link:allow-get-current-url`: Retrieve the URL that launched the app.
## 11. Opener (`tauri-plugin-opener`)
Open files or URLs using the system's default applications. Replaces `shell:open` for many v2 use cases.
**Install:**
```bash
cargo tauri add opener
```
**Rust registration** (in `src-tauri/src/lib.rs`):
```rust
.plugin(tauri_plugin_opener::init())
```
**JS Package:** `@tauri-apps/plugin-opener`
**Capability permissions** (add to `src-tauri/capabilities/*.json`):
```json
{
"permissions": ["opener:default"]
}
```
**Common permissions:**
- `opener:allow-open-url`: Open a website URL.
- `opener:allow-open-path`: Open a local file path with its associated app.
## 12. Process (`tauri-plugin-process`)
Control the application process (restart, exit).
**Install:**
```bash
cargo tauri add process
```
**Rust registration** (in `src-tauri/src/lib.rs`):
```rust
.plugin(tauri_plugin_process::init())
```
**JS Package:** `@tauri-apps/plugin-process`
**Capability permissions** (add to `src-tauri/capabilities/*.json`):
```json
{
"permissions": ["process:default"]
}
```
**Common permissions:**
- `process:allow-restart`: Restart the app.
- `process:allow-exit`: Exit the app programmatically.
---
**See also:** [Capabilities Reference](capabilities-reference.md) for the security model | [Updater/Distribution](updater-distribution-reference.md) for the updater plugin deployment | [Advanced Runtime](advanced-runtime-reference.md) for tray, sidecar, and deep-link plugins
references/README.md
# Tauri v2 References
Deep-dive reference documentation for Tauri v2 development. Use these when the main [`SKILL.md`](../SKILL.md) quick-start isn't enough.
## Reference Files
| File | Description | Key Topics |
|------|-------------|------------|
| [`capabilities-reference.md`](capabilities-reference.md) | **Security & Permissions** | Capability files, permissions, scopes, v1 vs v2 model |
| [`ipc-patterns.md`](ipc-patterns.md) | **IPC Decision Framework** | Commands vs Events vs Channels, typed `Channel<T>` |
| [`plugin-reference.md`](plugin-reference.md) | **Official Plugins** | Registration, JS package, and required capability permissions |
| [`updater-distribution-reference.md`](updater-distribution-reference.md) | **Updater & Distribution** | Signing, HTTPS endpoints, macOS/Windows/Linux packaging |
| [`advanced-runtime-reference.md`](advanced-runtime-reference.md) | **Advanced Runtime** | `TrayIconBuilder`, sidecars, deep links, custom protocols |
## Navigation Guide
- **New to Tauri v2 security?** → Start with [`capabilities-reference.md`](capabilities-reference.md) to understand the mandatory capability model.
- **Choosing an IPC method?** → See [`ipc-patterns.md`](ipc-patterns.md) for the "Commands vs Events vs Channels" decision matrix.
- **Adding a plugin?** → Check [`plugin-reference.md`](plugin-reference.md) for the specific permission strings you MUST add to your capabilities.
- **Shipping to production?** → See [`updater-distribution-reference.md`](updater-distribution-reference.md) for mandatory signing and update server requirements.
- **Tray icons, sidecars, or deep links?** → See [`advanced-runtime-reference.md`](advanced-runtime-reference.md) for v2-specific implementations.
*Last verified: 2026-04-02. Check [official Tauri changelog](https://github.com/tauri-apps/tauri/blob/dev/crates/tauri/CHANGELOG.md) for updates.*
references/updater-distribution-reference.md
# Tauri v2+ Updater & Distribution Reference
## Contents
- Part 1: Updater (tauri-plugin-updater)
- Part 2: Distribution and Signing
- Part 3: Bundle Configuration
> ⚠️ **Signing is MANDATORY for production updates.** Unsigned artifacts will be rejected by the Tauri updater. Production update endpoints MUST use HTTPS.
## Part 1: Updater (tauri-plugin-updater)
### Install
```bash
cargo tauri add updater
```
### Configuration (tauri.conf.json)
```json
{
"plugins": {
"updater": {
"active": true,
"endpoints": ["https://your-server.com/update/{{target}}/{{current_version}}"],
"pubkey": "BASE64_PUBLIC_KEY_HERE",
"dialog": true
}
}
}
```
- **Endpoints:** Array of HTTPS URLs.
- **Pubkey:** Base64 encoded public key.
- **Dialog:** Boolean to show built-in update dialog.
- **HTTPS:** Endpoints MUST be HTTPS in production.
### Key Generation
```bash
cargo tauri signer generate -w ~/.tauri/myapp.key
```
Outputs: private key file + public key string.
- Store private key SECURELY. Never commit to repo.
- Set `TAURI_SIGNING_PRIVATE_KEY` env var for CI/CD.
- Set `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` if key is encrypted.
- Add pubkey to `tauri.conf.json` `plugins.updater.pubkey`.
### Signed Build
```bash
TAURI_SIGNING_PRIVATE_KEY=... cargo tauri build
```
Produces: installer file + `.sig` signature file.
Both files must be served from your update server.
### Update Server Response Format
The update endpoint must return this JSON format:
```json
{
"version": "1.0.1",
"notes": "Bug fixes",
"pub_date": "2026-04-02T00:00:00Z",
"platforms": {
"darwin-aarch64": {
"signature": "<content of .sig file>",
"url": "https://your-server/MyApp_1.0.1_aarch64.dmg"
},
"windows-x86_64": {
"signature": "<content of .sig file>",
"url": "https://your-server/MyApp_1.0.1_x64-setup.exe"
}
}
}
```
### Capability Permission
The updater plugin requires capability permission: `updater:default`
### Checking for Updates in Code
```rust
use tauri_plugin_updater::UpdaterExt;
#[tauri::command]
async fn check_for_updates(app: tauri::AppHandle) -> Result<String, String> {
let update = app.updater().map_err(|e| e.to_string())?
.check().await.map_err(|e| e.to_string())?;
if let Some(update) = update {
update.download_and_install(|_, _| {}, || {})
.await.map_err(|e| e.to_string())?;
Ok("Updated".to_string())
} else {
Ok("Already up to date".to_string())
}
}
```
## Part 2: Distribution and Signing
### macOS
- Code signing requires Apple Developer certificate.
- Notarization required for distribution outside Mac App Store.
- **Environment vars:** `APPLE_CERTIFICATE`, `APPLE_CERTIFICATE_PASSWORD`, `APPLE_SIGNING_IDENTITY`, `APPLE_ID`, `APPLE_PASSWORD`, `APPLE_TEAM_ID`.
- Command: `cargo tauri build` handles signing/notarization with env vars set.
- **Bundle type:** `.dmg`, `.app`.
- macOS bundles for arm64 (Apple Silicon) and x86_64 are separate.
### Windows
- Code signing requires a code signing certificate (EV or OV).
- Without signing, SmartScreen warnings appear for users.
- Self-signed certs are only suitable for development.
- **Env vars for signing:** via `TAURI_WINDOWS_SIGNING_CERTIFICATE` or custom script.
- **Bundle types:** `.msi` (WiX), `.exe` (NSIS).
- `bundle.windows.certificateThumbprint` in `tauri.conf.json` for direct cert config.
### Linux
- No mandatory code signing, but packaging for distros matters.
- **Bundle types:** `.deb` (Debian/Ubuntu), `.rpm` (Fedora/RHEL), `.AppImage` (universal).
- AppImage is portable but unsigned.
- For store distribution: use appropriate store SDK.
## Part 3: Bundle Configuration
Key `bundle` section in `tauri.conf.json`:
```json
{
"bundle": {
"active": true,
"targets": "all",
"identifier": "com.example.myapp",
"icon": ["icons/32x32.png", "icons/icon.icns", "icons/icon.ico"],
"resources": [],
"copyright": "",
"category": "Utility",
"shortDescription": "",
"longDescription": ""
}
}
```
> *Last verified: 2026-04-02. Check the [updater plugin changelog](https://github.com/tauri-apps/plugins-workspace/blob/v2/plugins/updater/CHANGELOG.md) for any updates to the signing/key format.*
SKILL.md
---
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