Tauri 2.x Desktop & Mobile Apps
SkillAI & modelsTauri 2.x commands, IPC bridge, permission system, plugins, window management, system tray, packaging
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Tauri 2.x Desktop & Mobile Apps skill
What this skill tells your AI
The instructions your AI receives, as published by agents-inc/skills in src/skills/desktop-framework-tauri/SKILL.md and read by ahel’s review.
Quick Guide: Tauri 2.x uses system webviews (not bundled Chromium) with a Rust backend. Define Rust commands with
#[tauri::command], invoke from frontend viainvoke()from@tauri-apps/api/core. Every sensitive operation requires an explicit permission grant in a capability file. Plugins follow a dual-install pattern: Cargo crate + npm package. Tauri 2 supports desktop (Windows, macOS, Linux) and mobile (iOS, Android).Current version: Tauri 2.x (stable, 2024+). Tauri 1.x is legacy and uses a fundamentally different security model (allowlist vs capabilities).
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use the Tauri 2.x capability/permission system -- the v1 allowlist is removed)
(You MUST register every command in tauri::generate_handler![] -- unregistered commands silently fail on invoke)
(You MUST add plugin permissions to a capability file -- plugins with missing permissions throw runtime errors)
(You MUST use #[cfg_attr(mobile, tauri::mobile_entry_point)] on pub fn run() for mobile support)
(You MUST use @tauri-apps/api/core for invoke() -- not the removed @tauri-apps/api/tauri path from v1)
</critical_requirements>
Auto-detection: Tauri, tauri.conf.json, src-tauri, tauri::command, tauri::Builder, invoke, @tauri-apps/api, tauri-plugin, capabilities, #[tauri::command], generate_handler, AppHandle, WebviewWindow, TrayIconBuilder
When to use:
- Building desktop apps with system webview + Rust backend
- Defining Rust commands and invoking them from frontend JavaScript/TypeScript
- Configuring the capability/permission security model
- Using official Tauri plugins (fs, dialog, http, notification, store, shell, etc.)
- System tray, window management, menus
- Packaging and distributing desktop or mobile apps
- Migrating from Tauri v1 to v2
When NOT to use:
- Frontend framework patterns (component architecture, state management, routing -- use respective framework skills)
- General Rust programming not related to Tauri APIs
- Build tool configuration (bundler, dev server -- separate tooling skill)
- If you need full Chromium features (WebRTC, Chrome DevTools Protocol, Chrome extensions -- evaluate alternatives)
Key patterns covered:
- Rust commands + frontend invoke bridge (examples/core.md)
- State management via
app.manage()+tauri::State<T>(examples/core.md) - Event system: emit/listen between frontend and backend (examples/core.md)
- Permission/capability system (examples/security.md)
- Official plugin installation and usage (examples/plugins.md)
- Window management, system tray, menus (examples/platform.md)
- Packaging and distribution (examples/packaging.md)
Detailed resources:
- examples/core.md - Commands, invoke, state, events, async commands, error handling
- examples/security.md - Capabilities, permissions, scopes, CSP, custom permissions
- examples/plugins.md - Official plugin registry, installation pattern, common plugins
- examples/platform.md - Windows, system tray, menus, multi-window, webview management
- examples/packaging.md - Build config, platform targets, updater, CI/CD
- reference.md - CLI commands, config reference, path variables, migration checklist
Philosophy
Tauri is security-first, small, and native. It uses the OS system webview instead of bundling Chromium, producing binaries 10-100x smaller than alternatives. The Rust backend provides memory safety and native performance. The permission system enforces least-privilege access -- nothing is allowed unless explicitly granted.
Tauri vs alternatives -- when Tauri is the right choice:
- You want small binary sizes (5-15 MB vs 150+ MB)
- You want native OS integration without bundling a browser engine
- You need a strong security model with granular permissions
- You are comfortable with Rust for backend logic
- You need mobile support (iOS/Android) from the same codebase
When Tauri may NOT be the right choice:
- You need guaranteed identical rendering across platforms (Tauri uses the OS webview, which varies)
- You need Chrome-specific APIs (WebRTC, Chrome extensions, Pepper plugins)
- Your team has no Rust experience and cannot invest in learning it
- You need WebView2 features not available in older Windows WebView2 versions
Core Patterns
Pattern 1: Rust Commands + Frontend Invoke
Define commands in Rust with #[tauri::command], register them with generate_handler![], invoke from frontend. Commands support arguments, return values, async, and error handling.
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
// Register in main.rs or lib.rs
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
import { invoke } from "@tauri-apps/api/core";
const greeting = await invoke<string>("greet", { name: "World" });
Key point: Arguments are passed as a single object. The Rust parameter names must match the object keys. Forgetting to register a command in generate_handler![] causes silent failure. See examples/core.md for async commands, error handling, and state access.
Pattern 2: Permission / Capability System
Every Tauri 2 app needs at least one capability file granting permissions. Without permissions, plugin and core API calls fail at runtime.
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main-capability",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"shell:allow-open",
"dialog:default",
{
"identifier": "fs:allow-write-text-file",
"allow": [{ "path": "$APPDATA/*" }]
}
]
}
Key point: Permissions are scoped to specific windows. Use path variables ($APPDATA, $HOME, etc.) to restrict filesystem access. The v1 allowlist is completely removed. See examples/security.md for the full permission model.
Pattern 3: State Management
Share state between commands using app.manage() and tauri::State<T>. For mutable state, wrap in Mutex or RwLock.
use std::sync::Mutex;
struct AppState {
counter: Mutex<i32>,
}
#[tauri::command]
fn increment(state: tauri::State<AppState>) -> i32 {
let mut counter = state.counter.lock().unwrap();
*counter += 1;
*counter
}
Key point: tauri::State<T> is injected automatically when declared as a command parameter. The type must implement Send + Sync. See examples/core.md for full patterns.
Pattern 4: Event System
Bidirectional events between frontend and backend. Frontend uses emit()/listen(), backend uses app.emit()/app.listen().
import { listen } from "@tauri-apps/api/event";
const unlisten = await listen<string>("download-progress", (event) => {
console.log(`Progress: ${event.payload}`);
});
// Clean up when done
unlisten();
// Emit from backend to all windows
app.emit("download-progress", "50%").unwrap();
Key point: Always call the unlisten function to prevent memory leaks. Events are string-typed -- use consistent naming conventions. See examples/core.md for targeted window events and channels.
Pattern 5: Plugin Installation Pattern
All official plugins follow a dual-install pattern: Rust crate + npm package. Each plugin needs permissions in a capability file.
# 1. Add Rust crate
cargo add tauri-plugin-store
# 2. Add JS bindings
npm add @tauri-apps/plugin-store
# 3. Register plugin in Rust
tauri::Builder::default()
.plugin(tauri_plugin_store::Builder::new().build())
# 4. Add permission to capability file
# "store:allow-get", "store:allow-set"
Key point: Missing any of the four steps (crate, npm, registration, permission) causes runtime errors, not compile errors. See examples/plugins.md for the full plugin list.
Pattern 6: System Tray
Build system tray icons with menus and event handlers.
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
use tauri::menu::{MenuBuilder, MenuItemBuilder};
tauri::Builder::default()
.setup(|app| {
let toggle = MenuItemBuilder::with_id("toggle", "Toggle").build(app)?;
let menu = MenuBuilder::new(app).items(&[&toggle]).build()?;
TrayIconBuilder::new()
.menu(&menu)
.on_menu_event(move |_app, event| match event.id().as_ref() {
"toggle" => println!("toggle clicked"),
_ => (),
})
.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(())
})
Key point: In Tauri 2, SystemTray is replaced by TrayIconBuilder. Menu events and tray icon events are handled separately. See examples/platform.md for full tray patterns.
<decision_framework>
Decision Framework
Tauri 2 App Architecture
Where does this logic belong?
|-- Pure UI rendering, user interaction?
| +-- Frontend (JavaScript/TypeScript in webview)
|-- File system, network, OS integration, heavy computation?
| +-- Rust backend (Tauri commands)
|-- Sensitive operation (file write, shell exec, HTTP)?
| +-- Rust command + explicit permission in capability file
+-- Shared state between commands?
+-- app.manage() with Mutex/RwLock wrapper
Command Design
How to structure the command?
|-- Returns data synchronously?
| +-- Regular #[tauri::command] fn
|-- Needs I/O, network, or long computation?
| +-- async #[tauri::command] with Result<T, E>
|-- Needs to access managed state?
| +-- Add tauri::State<T> parameter
|-- Needs app handle (emit events, manage windows)?
| +-- Add app: tauri::AppHandle parameter
+-- Needs to stream data to frontend?
+-- Use tauri::ipc::Channel<T> parameter
Plugin Selection
Need OS integration?
|-- File system access?
| +-- tauri-plugin-fs
|-- File/folder picker dialog?
| +-- tauri-plugin-dialog
|-- HTTP requests from backend?
| +-- tauri-plugin-http
|-- Persistent key-value storage?
| +-- tauri-plugin-store
|-- System notifications?
| +-- tauri-plugin-notification
|-- Run external processes?
| +-- tauri-plugin-shell
|-- Auto-update?
| +-- tauri-plugin-updater
|-- Clipboard?
| +-- tauri-plugin-clipboard-manager
|-- Deep links (custom URL scheme)?
| +-- tauri-plugin-deep-link
+-- Launch on system startup?
+-- tauri-plugin-autostart
See reference.md for CLI commands and config reference.
</decision_framework>
<red_flags>
RED FLAGS
High Priority Issues:
- Using
@tauri-apps/api/tauriimport path (removed in v2 -- use@tauri-apps/api/core) - Using the v1
allowlistintauri.conf.json(replaced by capability files in v2) - Forgetting to register commands in
generate_handler - Missing plugin permissions in capability file (runtime error, not compile error)
- Using
SystemTrayAPI (removed in v2 -- useTrayIconBuilderfromtauri::tray) - Using
tauri::api::*(removed in v2 -- functionality moved to plugins) - Calling
invoke()without awaiting (returns a Promise, not the value) - Missing
#[cfg_attr(mobile, tauri::mobile_entry_point)]onrun()(breaks mobile builds)
Medium Priority Issues:
- Not cleaning up event listeners (memory leaks from
listen()without calling unlisten) - Using
unwrap()in commands instead of returningResult(crashes the command, no error to frontend) - Granting overly broad permissions (
fs:allow-read-filewithout path scope) - Hardcoding paths instead of using Tauri path variables (
$APPDATA,$HOME,$RESOURCE) - Not using
Mutex/RwLockfor mutable managed state (data races)
Common Mistakes:
- Command argument name mismatch between Rust parameter and JS invoke object key
- Forgetting the npm package when installing a plugin (only adding the Cargo crate)
- Using
window.__TAURI__without settingapp.withGlobalTauri: truein config - Expecting identical webview rendering across platforms (Windows WebView2 vs macOS WebKit vs Linux WebKitGTK)
- Not handling the
Resulterror variant in async commands (unhandled promise rejection in frontend)
Gotchas & Edge Cases:
- Capability scoping: permissions are per-window -- a secondary window needs its own capability or must be listed in
windows - Path variables:
$APPDATA,$RESOURCE,$HOMEetc. are Tauri-specific, not environment variables - Mobile differences: some plugins are desktop-only (shell, autostart, global-shortcut), check plugin docs
- WebView2 on Windows: requires WebView2 runtime (bundled by default in installer, but not guaranteed on older Windows 10)
- Serialization: command arguments and return values must be
serde::Serialize/Deserialize-- complex types need derive macros - Dev server:
tauri devproxies your frontend dev server -- configuredevUrlintauri.conf.json, not in the frontend build tool - Build size: Tauri binaries are 5-15 MB, but Rust compilation is slow -- expect 2-5 min clean builds
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use the Tauri 2.x capability/permission system -- the v1 allowlist is removed)
(You MUST register every command in tauri::generate_handler![] -- unregistered commands silently fail on invoke)
(You MUST add plugin permissions to a capability file -- plugins with missing permissions throw runtime errors)
(You MUST use #[cfg_attr(mobile, tauri::mobile_entry_point)] on pub fn run() for mobile support)
(You MUST use @tauri-apps/api/core for invoke() -- not the removed @tauri-apps/api/tauri path from v1)
Failure to follow these rules will cause silent command failures, runtime permission errors, or broken mobile builds.
</critical_reminders>
Signals
- GitHub stars
- 24
- Forks
- 8
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
desktop-framework-tauri- Source
- github.com/agents-inc/skills