
Tauri V2
- 6.5k installs
- 14 repo stars
- Updated April 20, 2026
- nodnarbnitram/claude-code-extensions
tauri-v2 is an agent skill for Tauri v2+ cross-platform apps with Rust commands, IPC, capabilities, and build troubleshooting.
About
The tauri-v2 skill guides Tauri v2+ cross-platform app development with a web frontend and Rust backend. Use when configuring tauri.conf.json, implementing #[tauri::command] handlers, setting up IPC via invoke, emit, and channels, configuring capabilities.json permissions, troubleshooting builds, or deploying desktop and mobile apps. Quick start covers registering commands in generate_handler!, calling invoke from @tauri-apps/api/core (not v1 APIs), and granting core:default in capabilities because v2 denies everything by default. Critical rules require lib.rs for all application logic with mobile_entry_point, owned types in async commands, Mutex for shared state, and explicit plugin permissions. Documented patterns span Result error propagation across IPC, serde camelCase and snake_case boundaries, state management, event emission, channel streaming, and WebviewWindow access in v2. Troubleshooting tables address permission denied, missing handlers, white screens from devUrl mismatch, mobile target gaps, and desktop-only plugin pitfalls. Bundled references cover capabilities, IPC, plugins, updater signing, and advanced runtime features.
- Registers #[tauri::command] handlers in generate_handler! and calls invoke from @tauri-apps/api/core.
- Grants explicit capabilities because Tauri v2 denies operations by default.
- Places all logic in lib.rs with mobile_entry_point for cross-platform mobile builds.
- Documents IPC patterns for invoke, emit, channels, state Mutex, and serde boundaries.
- Troubleshooting tables cover permissions, white screens, mobile targets, and plugin gaps.
Tauri V2 by the numbers
- 6,521 all-time installs (skills.sh)
- +521 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #3 of 129 Rust skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
tauri-v2 capabilities & compatibility
- Capabilities
- tauri command registration and frontend invoke w · capabilities and plugin permission configuration · ipc patterns for events, channels, and shared mu · tauri.conf.json devurl, bundle, and security set · error handling with result and serde across ipc · troubleshooting white screens, mobile targets, a
- Use cases
- frontend · api development
- Platforms
- macOS · Windows · Linux
- Runs
- Runs locally
What tauri-v2 says it does
Build cross-platform desktop and mobile apps with web frontends and Rust backends.
Tauri v2 denies everything by default - explicit permissions required for all operations.
npx skills add https://github.com/nodnarbnitram/claude-code-extensions --skill tauri-v2Add your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 6.5k |
|---|---|
| repo stars | ★ 14 |
| Security audit | 2 / 3 scanners passed |
| Last updated | April 20, 2026 |
| Repository | nodnarbnitram/claude-code-extensions ↗ |
How do I configure Tauri v2 commands, permissions, IPC, and mobile builds without silent handler failures or permission denials?
Tauri v2 cross-platform desktop and mobile apps with Rust commands, IPC, capabilities, and deployment troubleshooting.
Who is it for?
Developers shipping desktop or mobile apps with Tauri v2, Rust backends, and web frontends who need IPC and permission guidance.
Skip if: Skip for pure web-only Next.js or server API work without a Tauri desktop or mobile shell.
When should I use this skill?
User configures tauri.conf.json, implements Rust commands, sets up invoke or emit IPC, edits capabilities.json, or debugs Tauri v2 builds.
What you get
Working Tauri v2 apps with registered commands, explicit capabilities, documented IPC patterns, and resolved build issues.
- Rust command modules
- IPC and security configuration
By the numbers
- Skill version 1.0.1 last updated 2026-04-02
- Confidence rating 4/5 with production testing on v2.tauri.app
Files
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
// 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.rsshould only be a thin passthrough — all application logic lives inlib.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 replacesmain()withmobile_entry_pointon mobile targets.
Step 2: Call from Frontend
import { invoke } from '@tauri-apps/api/core';
const greeting = await invoke<string>('greet', { name: 'World' });
console.log(greeting); // "Hello, World!"Why this matters: Use @tauri-apps/api/core (not @tauri-apps/api/tauri - that's v1 API).
Step 3: Add Required Permissions
// src-tauri/capabilities/default.json
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"windows": ["main"],
"permissions": ["core:default"]
}Why this matters: Tauri v2 denies everything by default - explicit permissions required for all operations.
Critical Rules
Always Do
- Register every command in
tauri::generate_handler![cmd1, cmd2, ...] - Return
Result<T, E>from commands for proper error handling - Use
Mutex<T>for shared state accessed from multiple commands - Add capabilities before using any plugin features
- Use
lib.rsfor shared code (required for mobile builds) - Use
#[cfg_attr(mobile, tauri::mobile_entry_point)]onpub fn run()inlib.rsfor 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:
#[tauri::command]
async fn bad(name: &str) -> String { // Compile error!
name.to_string()
}Correct - Owned type:
#[tauri::command]
async fn good(name: String) -> String {
name
}Why: Async commands cannot borrow data across await points; Tauri requires owned types for async command parameters.
Known Issues Prevention
| Issue | Root Cause | Solution |
|---|---|---|
| "Command not found" | Missing from generate_handler! | Add command to handler macro |
| "Permission denied" | Missing capability | Add to capabilities/default.json |
| 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`
- IPC decision guide → `references/ipc-patterns.md`
- Official plugins → `references/plugin-reference.md`
- Updater & distribution → `references/updater-distribution-reference.md`
- Tray, sidecars, deep links → `references/advanced-runtime-reference.md`
Configuration Reference
tauri.conf.json
{
"$schema": "./gen/schemas/desktop-schema.json",
"productName": "my-app",
"version": "1.0.0",
"identifier": "com.example.myapp",
"build": {
"devUrl": "http://localhost:5173",
"frontendDist": "../dist",
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build"
},
"app": {
"windows": [{
"label": "main",
"title": "My App",
"width": 800,
"height": 600
}],
"security": {
"csp": "default-src 'self'; img-src 'self' data:",
"capabilities": ["default"]
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": ["icons/icon.icns", "icons/icon.ico", "icons/icon.png"]
}
}Key settings:
build.devUrl: Must match your frontend dev server portapp.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.jsonWhy `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
[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 buildscrate-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` for full implementation details.
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.
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 beundefinedornull)- 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` for more complex state patterns.
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` for bidirectional examples.
use tauri::Emitter;
#[tauri::command]
fn start_task(app: tauri::AppHandle) {
std::thread::spawn(move || {
app.emit("task-progress", 50).unwrap();
app.emit("task-complete", "done").unwrap();
});
}import { listen } from '@tauri-apps/api/event';
const unlisten = await listen('task-progress', (e) => {
console.log('Progress:', e.payload);
});
// Call unlisten() when doneChannel Streaming Pattern
Channels provide high-frequency, typed streaming from Rust to Frontend. See `references/ipc-patterns.md` for full implementation details.
use tauri::ipc::Channel;
#[derive(Clone, serde::Serialize)]
#[serde(tag = "event", content = "data")]
enum DownloadEvent {
Progress { percent: u32 },
Complete { path: String },
}
#[tauri::command]
async fn download(url: String, on_event: Channel<DownloadEvent>) {
for i in 0..=100 {
on_event.send(DownloadEvent::Progress { percent: i }).unwrap();
}
on_event.send(DownloadEvent::Complete { path: "/downloads/file".into() }).unwrap();
}import { invoke, Channel } from '@tauri-apps/api/core';
const channel = new Channel<DownloadEvent>();
channel.onmessage = (msg) => console.log(msg.event, msg.data);
await invoke('download', { url: 'https://...', onEvent: channel });Window Access Pattern
Tauri v2 uses WebviewWindow for unified window and webview management.
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` - Permission patterns and examples
- `ipc-patterns.md` - Complete IPC examples
- `plugin-reference.md` - Official plugin install, registration, and permission strings
- `updater-distribution-reference.md` - Signing, HTTPS requirements, and bundle shipping
- `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 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` for full install + permission details for all official plugins.
Official Documentation
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:
# 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-simDesktop 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
#[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 infoshows correct Tauri v2 versions - [ ]
src-tauri/capabilities/default.jsonexists with at leastcore:default - [ ] All commands registered in
generate_handler![] - [ ]
lib.rscontains shared code (for mobile support) - [ ] Required Rust targets installed for target platforms
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 and release notes fortauri,@tauri-apps/api,@tauri-apps/cli, and relevant plugins.
Quick Usage
# 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 buildToken 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` | V2 security model, capability files, permissions, and scopes |
| IPC Patterns | `ipc-patterns.md` | Decision framework for Commands, Events, and Channels |
| Official Plugins | `plugin-reference.md` | Install, registration, and permissions for all official plugins |
| Updater & Distribution | `updater-distribution-reference.md` | Signing, updater setup, and platform-specific distribution |
| Advanced Runtime | `advanced-runtime-reference.md` | Tray icons, sidecars, deep links, and custom protocols |
See the References Index 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, protocolsDependencies
| 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 for feature timing.*
Official Documentation
- Tauri v2+ Documentation
- Commands Reference
- IPC Concepts
- Capabilities & Permissions
- Configuration Reference
- Plugin Directory
Related Skills
tanstack-start-expert- TanStack Router patterns for type-safe frontend routingreact-component-architect- React component patterns for Tauri frontendsgo-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
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 installation and permissions
- capabilities-reference.md — capability/permission model
Section 1: System Tray (TrayIconBuilder)
v2 Change:SystemTrayfrom v1 is replaced byTrayIconBuilderin v2. Do NOT useSystemTray.
// 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:
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
libappindicatororlibayatana-appindicator)
Section 2: Sidecars (External Binaries)
Show config and usage for bundled executables:
// tauri.conf.json
{
"bundle": {
"externalBin": [
"binaries/my-sidecar"
]
}
}Capability permission required:
{
"permissions": [
{
"identifier": "shell:allow-execute",
"allow": [
{ "name": "my-sidecar", "args": true, "sidecar": true }
]
}
]
}Rust code to execute sidecar:
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)
cargo tauri add deep-linkConfig in tauri.conf.json:
{
"plugins": {
"deep-link": {
"mobile": [
{ "scheme": "myapp" }
],
"desktop": [
{ "schemes": ["myapp"] }
]
}
}
}Capability:
{ "permissions": ["deep-link:default"] }Handling deep links in 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 viainvoke_filterorasset_protocol) is a more advanced feature. The primary official pattern is the built-inassetprotocol for serving local files. Custom protocol handlers require careful security consideration.
Show asset protocol access (most common use case):
// tauri.conf.json
{
"app": {
"security": {
"assetScope": ["$APPDATA/assets/**", "$RESOURCE/**"]
}
}
}// 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.
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/
{
"$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)
{
"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)
{
"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:
{
"permissions": [
{
"identifier": "fs:allow-read-file",
"allow": [
{ "path": "$APPDATA/*" },
{ "path": "$HOME/Documents/*" }
]
}
]
}Dialog (tauri-plugin-dialog)
{
"permissions": [
"dialog:default",
"dialog:allow-open",
"dialog:allow-save",
"dialog:allow-message",
"dialog:allow-ask",
"dialog:allow-confirm"
]
}Shell (tauri-plugin-shell)
{
"permissions": [
"shell:default",
"shell:allow-open",
"shell:allow-execute"
]
}Scoped Execute:
{
"permissions": [
{
"identifier": "shell:allow-execute",
"allow": [
{ "name": "git", "args": true },
{ "name": "npm", "args": ["install", "run"] }
]
}
]
}HTTP (tauri-plugin-http)
{
"permissions": [
"http:default"
]
}With URL Scopes:
{
"permissions": [
{
"identifier": "http:default",
"allow": [
{ "url": "https://api.example.com/*" },
{ "url": "https://*.myapp.com/*" }
]
}
]
}Store (tauri-plugin-store)
{
"permissions": [
"store:default",
"store:allow-get",
"store:allow-set",
"store:allow-delete",
"store:allow-keys",
"store:allow-clear"
]
}Clipboard (tauri-plugin-clipboard-manager)
{
"permissions": [
"clipboard-manager:default",
"clipboard-manager:allow-read",
"clipboard-manager:allow-write"
]
}Notification (tauri-plugin-notification)
{
"permissions": [
"notification:default",
"notification:allow-send",
"notification:allow-request-permission"
]
}Global Shortcut (tauri-plugin-global-shortcut)
{
"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/.
{
"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
{
"identifier": "desktop-only",
"platforms": ["linux", "macos", "windows"],
"permissions": ["global-shortcut:default"]
}{
"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.
{
"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:
{
"identifier": "remote-access",
"remote": {
"urls": ["https://*.myapp.com"]
},
"permissions": ["http:default"]
}Custom Permission Files
Create custom permissions in src-tauri/permissions/:
`custom.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:
{
"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 for plugin-specific permission strings | Advanced Runtime 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
{
"identifier": "minimal",
"windows": ["main"],
"permissions": ["core:default"]
}File Manager
{
"identifier": "file-manager",
"windows": ["main"],
"permissions": [
"core:default",
"fs:default",
"dialog:allow-open",
"dialog:allow-save"
]
}Web-Connected App
{
"identifier": "web-app",
"windows": ["main"],
"permissions": [
"core:default",
"http:default",
"shell:allow-open"
]
}Full Desktop App
{
"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"
]
}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 for permission setup | Plugin Reference 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:
#[tauri::command]
fn greet(name: String) -> String {
format!("Hello, {}!", name)
}
// Register in builder
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet])Frontend:
import { invoke } from '@tauri-apps/api/core';
const result = await invoke<string>('greet', { name: 'World' });Command with Multiple Arguments
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:
const result = await invoke<number>('calculate', {
a: 10,
b: 5,
operation: 'add'
});Async Command
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:
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:
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:
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:
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:
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:
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:
// 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()onAppHandle/WebviewWindow.use tauri::Listener;to call.listen()onApp/AppHandle. These traits must be in scope.
Emit from Rust to Frontend
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:
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:
import { emit } from '@tauri-apps/api/event';
await emit('user-action', { action: 'click', target: 'button' });Rust (in setup or command):
use tauri::Listener;
fn setup_listeners(app: &tauri::App) {
app.listen("user-action", |event| {
println!("User action: {:?}", event.payload());
});
}Window-Specific Events
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>whereMyEvent: serde::Serialize + Clone - Frontend:
new Channel<MyEvent>()with matching TypeScript type - Use
#[serde(tag = "event", content = "data")]on enums for discriminated union patterns.
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:
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:
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:
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
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:
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:
cargo tauri add fsRust registration (in src-tauri/src/lib.rs):
.plugin(tauri_plugin_fs::init())JS Package: @tauri-apps/plugin-fs
Capability permissions (add to src-tauri/capabilities/*.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 for scope examples.
2. Dialog (tauri-plugin-dialog)
Native system dialogs for file picking and messages.
Install:
cargo tauri add dialogRust registration (in src-tauri/src/lib.rs):
.plugin(tauri_plugin_dialog::init())JS Package: @tauri-apps/plugin-dialog
Capability permissions (add to src-tauri/capabilities/*.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:
cargo tauri add shellRust registration (in src-tauri/src/lib.rs):
.plugin(tauri_plugin_shell::init())JS Package: @tauri-apps/plugin-shell
Capability permissions (add to src-tauri/capabilities/*.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 for shell scope examples.
4. HTTP (tauri-plugin-http)
Perform HTTP requests from the Rust backend (bypassing CORS).
Install:
cargo tauri add httpRust registration (in src-tauri/src/lib.rs):
.plugin(tauri_plugin_http::init())JS Package: @tauri-apps/plugin-http
Capability permissions (add to src-tauri/capabilities/*.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 for URL scope examples.
5. Store (tauri-plugin-store)
Simple key-value persistence.
Install:
cargo tauri add storeRust registration (in src-tauri/src/lib.rs):
.plugin(tauri_plugin_store::Builder::default().build())JS Package: @tauri-apps/plugin-store
Capability permissions (add to src-tauri/capabilities/*.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:
cargo tauri add clipboard-managerRust registration (in src-tauri/src/lib.rs):
.plugin(tauri_plugin_clipboard_manager::init())JS Package: @tauri-apps/plugin-clipboard-manager
Capability permissions (add to src-tauri/capabilities/*.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:
cargo tauri add notificationRust registration (in src-tauri/src/lib.rs):
.plugin(tauri_plugin_notification::init())JS Package: @tauri-apps/plugin-notification
Capability permissions (add to src-tauri/capabilities/*.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:
cargo tauri add global-shortcutRust registration (in src-tauri/src/lib.rs):
.plugin(tauri_plugin_global_shortcut::Builder::new().build())JS Package: @tauri-apps/plugin-global-shortcut
Capability permissions (add to src-tauri/capabilities/*.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:
cargo tauri add updaterRust registration (in src-tauri/src/lib.rs):
.plugin(tauri_plugin_updater::Builder::new().build())JS Package: @tauri-apps/plugin-updater
Capability permissions (add to src-tauri/capabilities/*.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 for signing requirements.
10. Deep Link (tauri-plugin-deep-link)
Register and handle custom URL schemes (e.g., myapp://).
Install:
cargo tauri add deep-linkRust registration (in src-tauri/src/lib.rs):
.plugin(tauri_plugin_deep_link::init())JS Package: @tauri-apps/plugin-deep-link
Capability permissions (add to src-tauri/capabilities/*.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:
cargo tauri add openerRust registration (in src-tauri/src/lib.rs):
.plugin(tauri_plugin_opener::init())JS Package: @tauri-apps/plugin-opener
Capability permissions (add to src-tauri/capabilities/*.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:
cargo tauri add processRust registration (in src-tauri/src/lib.rs):
.plugin(tauri_plugin_process::init())JS Package: @tauri-apps/plugin-process
Capability permissions (add to src-tauri/capabilities/*.json):
{
"permissions": ["process:default"]
}Common permissions:
process:allow-restart: Restart the app.process:allow-exit: Exit the app programmatically.
---
See also: Capabilities Reference for the security model | Updater/Distribution for the updater plugin deployment | Advanced Runtime for tray, sidecar, and deep-link plugins
Tauri v2 References
Deep-dive reference documentation for Tauri v2 development. Use these when the main `SKILL.md` quick-start isn't enough.
Reference Files
| File | Description | Key Topics |
|---|---|---|
| `capabilities-reference.md` | Security & Permissions | Capability files, permissions, scopes, v1 vs v2 model |
| `ipc-patterns.md` | IPC Decision Framework | Commands vs Events vs Channels, typed Channel<T> |
| `plugin-reference.md` | Official Plugins | Registration, JS package, and required capability permissions |
| `updater-distribution-reference.md` | Updater & Distribution | Signing, HTTPS endpoints, macOS/Windows/Linux packaging |
| `advanced-runtime-reference.md` | Advanced Runtime | TrayIconBuilder, sidecars, deep links, custom protocols |
Navigation Guide
- New to Tauri v2 security? → Start with `capabilities-reference.md` to understand the mandatory capability model.
- Choosing an IPC method? → See `ipc-patterns.md` for the "Commands vs Events vs Channels" decision matrix.
- Adding a plugin? → Check `plugin-reference.md` for the specific permission strings you MUST add to your capabilities.
- Shipping to production? → See `updater-distribution-reference.md` for mandatory signing and update server requirements.
- Tray icons, sidecars, or deep links? → See `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.
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
cargo tauri add updaterConfiguration (tauri.conf.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
cargo tauri signer generate -w ~/.tauri/myapp.keyOutputs: private key file + public key string.
- Store private key SECURELY. Never commit to repo.
- Set
TAURI_SIGNING_PRIVATE_KEYenv var for CI/CD. - Set
TAURI_SIGNING_PRIVATE_KEY_PASSWORDif key is encrypted. - Add pubkey to
tauri.conf.jsonplugins.updater.pubkey.
Signed Build
TAURI_SIGNING_PRIVATE_KEY=... cargo tauri buildProduces: 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:
{
"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
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 buildhandles 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_CERTIFICATEor custom script. - Bundle types:
.msi(WiX),.exe(NSIS). bundle.windows.certificateThumbprintintauri.conf.jsonfor 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:
{
"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.
Related skills
How it compares
Pick tauri-v2 over Electron skills when you want smaller binaries with Rust backend commands and Tauri v2 security manifests rather than Node-main-process desktop apps.
FAQ
Why do invoke calls return undefined in Tauri v2?
Verify the command is listed in generate_handler!, returns a value, and argument names match camelCase in JS and snake_case in Rust.
Where should Tauri application logic live?
Put all commands, state, and application setup in lib.rs::run() with mobile_entry_point; keep main.rs as a thin passthrough.
How do I fix permission denied errors?
Add required permission strings such as core:default or plugin permissions to src-tauri/capabilities/default.json before using features.
Is Tauri V2 safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.