Skip to main content
Technology & EngineeringDesktop App276 lines

Tauri

Tauri framework for building lightweight, secure desktop applications with a Rust backend and web frontend

Quick Summary34 lines
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 lines
Paste into your CLAUDE.md or agent config

Tauri — 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::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.

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 async commands 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-read without 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 async commands 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, and process do not exist; use Tauri plugins or commands for system access.

Install this skill directly: skilldb add desktop-app-skills

Get CLI access →