Tauri
Tauri framework for building lightweight, secure desktop applications with a Rust backend and web frontend
You are an expert in Tauri for building lightweight, secure desktop applications with a Rust backend and any web frontend framework. ## Key Points - **Rust core**: Manages windows, system APIs, and application logic - **Web frontend**: Any framework (React, Svelte, Vue, plain HTML) rendered in the native webview - **Command system**: Type-safe bridge between frontend JavaScript and Rust backend - **Plugin system**: Extend functionality with official and community plugins - Use Tauri's permission system in `capabilities/` to grant only the APIs each window actually needs. - Return `Result<T, String>` from commands so errors propagate cleanly to the frontend. - Use `tauri::async_runtime::spawn` for long-running Rust tasks to avoid blocking the main thread. - Leverage `tauri::State` for shared state instead of global variables. - Keep Rust commands focused and thin — delegate business logic to separate modules. - Use the official plugins (`tauri-plugin-fs`, `tauri-plugin-dialog`, `tauri-plugin-store`) instead of reimplementing native functionality. - Set a strict Content Security Policy in `tauri.conf.json` to limit what the webview can load. - **Forgetting to register commands**: Commands must be listed in `generate_handler![]` or they silently won't work. ## Quick Example ```bash cargo tauri init # Or with create-tauri-app scaffolder: npm create tauri-app@latest ``` ```toml # src-tauri/Cargo.toml [dependencies] tauri-plugin-fs = "2" tauri-plugin-dialog = "2" tauri-plugin-shell = "2" ```
skilldb get desktop-app-skills/TauriFull skill: 276 linesTauri — Desktop Apps
You are an expert in Tauri for building lightweight, secure desktop applications with a Rust backend and any web frontend framework.
Overview
Tauri builds desktop apps using the OS-native webview (WebView2 on Windows, WebKit on macOS/Linux) instead of bundling Chromium. The result is dramatically smaller binaries (often under 10 MB) and lower memory usage compared to Electron. The backend is written in Rust, providing memory safety, high performance, and fine-grained security controls through a permission system.
Key architecture:
- Rust core: Manages windows, system APIs, and application logic
- Web frontend: Any framework (React, Svelte, Vue, plain HTML) rendered in the native webview
- Command system: Type-safe bridge between frontend JavaScript and Rust backend
- Plugin system: Extend functionality with official and community plugins
Setup & Configuration
Prerequisites
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Install Tauri CLI
cargo install tauri-cli
# Or via npm
npm install --save-dev @tauri-apps/cli
Create a new project
cargo tauri init
# Or with create-tauri-app scaffolder:
npm create tauri-app@latest
Project structure
my-tauri-app/
├── src/ # Frontend source
│ ├── index.html
│ └── main.js
├── src-tauri/
│ ├── Cargo.toml # Rust dependencies
│ ├── tauri.conf.json # Tauri configuration
│ ├── capabilities/ # Permission definitions
│ ├── src/
│ │ ├── main.rs # Rust entry point
│ │ └── lib.rs # Command definitions
│ └── icons/ # App icons
└── package.json
tauri.conf.json essentials
{
"productName": "My App",
"version": "1.0.0",
"identifier": "com.mycompany.myapp",
"build": {
"frontendDist": "../dist",
"devUrl": "http://localhost:5173",
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build"
},
"app": {
"windows": [
{
"title": "My App",
"width": 1200,
"height": 800,
"resizable": true,
"fullscreen": false
}
],
"security": {
"csp": "default-src 'self'; style-src 'self' 'unsafe-inline'"
}
}
}
Core Patterns
Defining Rust commands
// src-tauri/src/lib.rs
use tauri::Manager;
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! Welcome to Tauri.", name)
}
#[tauri::command]
async fn read_config(app: tauri::AppHandle) -> Result<String, String> {
let config_path = app
.path()
.app_config_dir()
.map_err(|e| e.to_string())?
.join("config.json");
std::fs::read_to_string(&config_path)
.map_err(|e| format!("Failed to read config: {}", e))
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet, read_config])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Calling commands from the frontend
import { invoke } from '@tauri-apps/api/core';
// Call a simple command
const greeting = await invoke('greet', { name: 'World' });
console.log(greeting); // "Hello, World! Welcome to Tauri."
// Call an async command with error handling
try {
const config = await invoke('read_config');
console.log(JSON.parse(config));
} catch (err) {
console.error('Failed to load config:', err);
}
Event system
// Emit from Rust to frontend
use tauri::Emitter;
#[tauri::command]
fn start_processing(app: tauri::AppHandle) {
std::thread::spawn(move || {
for i in 0..100 {
app.emit("progress", i).unwrap();
std::thread::sleep(std::time::Duration::from_millis(50));
}
app.emit("complete", "Done!").unwrap();
});
}
// Listen in frontend
import { listen } from '@tauri-apps/api/event';
const unlisten = await listen('progress', (event) => {
console.log(`Progress: ${event.payload}%`);
});
// Clean up listener
unlisten();
State management in Rust
use std::sync::Mutex;
use tauri::Manager;
struct AppState {
counter: Mutex<i32>,
db_path: String,
}
#[tauri::command]
fn increment(state: tauri::State<'_, AppState>) -> i32 {
let mut counter = state.counter.lock().unwrap();
*counter += 1;
*counter
}
pub fn run() {
tauri::Builder::default()
.manage(AppState {
counter: Mutex::new(0),
db_path: String::from("app.db"),
})
.invoke_handler(tauri::generate_handler![increment])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Using official plugins
# src-tauri/Cargo.toml
[dependencies]
tauri-plugin-fs = "2"
tauri-plugin-dialog = "2"
tauri-plugin-shell = "2"
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_shell::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
import { open } from '@tauri-apps/plugin-dialog';
import { readTextFile } from '@tauri-apps/plugin-fs';
const filePath = await open({
filters: [{ name: 'Text', extensions: ['txt', 'md'] }],
});
if (filePath) {
const contents = await readTextFile(filePath);
console.log(contents);
}
Best Practices
- Use Tauri's permission system in
capabilities/to grant only the APIs each window actually needs. - Return
Result<T, String>from commands so errors propagate cleanly to the frontend. - Use
tauri::async_runtime::spawnfor long-running Rust tasks to avoid blocking the main thread. - Leverage
tauri::Statefor shared state instead of global variables. - Keep Rust commands focused and thin — delegate business logic to separate modules.
- Use the official plugins (
tauri-plugin-fs,tauri-plugin-dialog,tauri-plugin-store) instead of reimplementing native functionality. - Set a strict Content Security Policy in
tauri.conf.jsonto limit what the webview can load.
Common Pitfalls
- Forgetting to register commands: Commands must be listed in
generate_handler![]or they silently won't work. - Blocking the main thread in Rust: Synchronous I/O in a command blocks the webview. Use
asynccommands for I/O. - Missing permissions: Tauri v2 requires explicit capability grants. A command will fail at runtime if the required permission is not declared.
- Assuming Node.js APIs exist: The frontend runs in a webview, not Node.js. Use Tauri plugins or commands for file system, shell, and OS access.
- Large payloads over invoke: Serializing large data (multi-MB) through the command bridge is slow. For large files, write to a temp path in Rust and pass the path to the frontend.
- Not handling platform differences: Webview behavior varies across Windows (WebView2), macOS (WebKit), and Linux (WebKitGTK). Test on all targets.
Core Philosophy
Tauri's architecture embodies a clear separation of concerns: Rust handles system access, security, and performance-sensitive logic, while the web frontend handles what it does best — building user interfaces. Resist the temptation to push business logic into the frontend just because it is faster to prototype in JavaScript. The Rust backend is where file operations, database queries, cryptography, and system interactions belong, secured by Tauri's capability-based permission model.
Security by default is Tauri's defining advantage over Electron. The permission system ensures that each window can only access explicitly granted capabilities. A settings window should not have file system access. A login window should not have shell access. Design your capability files with the principle of least privilege, granting each window only the APIs it genuinely needs.
Embrace Rust for what it offers: memory safety, zero-cost abstractions, and fearless concurrency. You do not need to be a Rust expert to build a Tauri app, but investing in basic Rust proficiency — error handling with Result, async functions, and struct design — unlocks Tauri's full potential. The frontend-backend command bridge is ergonomic enough that most interactions feel natural even for developers new to Rust.
Anti-Patterns
-
Putting business logic in the frontend — performing file I/O, database queries, or system operations in JavaScript via plugin APIs when they should be Rust commands with proper validation and error handling wastes Tauri's security model.
-
Granting broad permissions without scoping — declaring
fs:allow-readwithout a scope restriction gives the frontend access to the entire file system; always scope permissions to specific directories. -
Forgetting to register commands — commands not listed in
generate_handler![]silently fail when invoked from the frontend, producing confusing "command not found" errors with no compile-time warning. -
Blocking the main thread with synchronous I/O — synchronous file reads or network calls in command handlers block the webview; use
asynccommands for all I/O operations. -
Assuming Node.js APIs exist in the frontend — the Tauri frontend runs in a webview, not Node.js;
require,fs,path, andprocessdo not exist; use Tauri plugins or commands for system access.
Install this skill directly: skilldb add desktop-app-skills
Related Skills
Auto Update
Auto-update mechanisms for desktop applications using electron-updater and Tauri's built-in updater
Electron Ipc
IPC communication patterns for Electron main and renderer process messaging
Electron
Electron fundamentals for building cross-platform desktop applications with web technologies
File System Access
File system access patterns, native dialogs, and drag-and-drop for desktop applications
Native Menus
System tray icons, native application menus, and desktop notifications for Electron and Tauri apps
Packaging
Building, packaging, and distributing desktop applications for Windows, macOS, and Linux