Use when adding new error messages to React, or seeing "unknown error code" warnings.
npx skills add Nghi-NV/create-agent-skills --skill "tauri-v2"
Install specific skill from multi-skill repository
# Description
Guide for building professional desktop apps with Tauri 2.0, Rust backend, and MVVM React frontend. Use when creating cross-platform apps with Vite + React + Zustand + Tailwind CSS 4.
# SKILL.md
name: tauri-v2
description: Guide for building professional desktop apps with Tauri 2.0, Rust backend, and MVVM React frontend. Use when creating cross-platform apps with Vite + React + Zustand + Tailwind CSS 4.
Tauri 2.0 Desktop App Development
This skill provides guidance for building professional cross-platform desktop applications using Tauri 2.0 with a Rust backend and modern React frontend following MVVM architecture.
When to Use This Skill
- Building cross-platform desktop apps (Windows, macOS, Linux)
- Migrating from Electron to Tauri for smaller bundle size
- Creating secure, performant native apps with web technologies
- Implementing complex state management between Rust and React
[!CAUTION]
This skill is for Tauri 2.0 only. Tauri 1.x uses different APIs and configuration.
Prerequisites
- Rust: Install via rustup
- Node.js: 18+ LTS
- Platform tools:
- macOS: Xcode Command Line Tools
- Windows: Visual Studio Build Tools + WebView2
- Linux:
webkit2gtk,libayatana-appindicator
Project Setup
Quick Start
# Create new project with React + TypeScript
npm create tauri-app@latest my-app -- --template react-ts
cd my-app
npm create tauri-app@latest my-app -- --template react-ts --identifier com.lumi.lumiiot --manager yarn --force true
# Install frontend dependencies
npm install zustand react-router-dom
npm install tailwindcss @tailwindcss/vite -D
# Run development
npm run tauri dev
Tailwind CSS 4 Setup
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [react(), tailwindcss()],
});
/* src/styles/globals.css */
@import "tailwindcss";
@theme {
--color-primary: oklch(0.6 0.2 250);
--color-secondary: oklch(0.7 0.15 180);
--font-sans: "Inter", system-ui, sans-serif;
}
Rust Backend Architecture
Module Organization
src-tauri/src/
βββ main.rs # Entry point (minimal)
βββ lib.rs # App builder, state/plugin registration
βββ commands/ # Tauri commands by feature
β βββ mod.rs
β βββ file.rs
β βββ settings.rs
βββ services/ # Business logic (pure Rust)
β βββ mod.rs
β βββ storage.rs
βββ models/ # Data structures
β βββ mod.rs
βββ state/ # App state management
β βββ mod.rs
βββ plugins/ # Custom Tauri plugins
β βββ mod.rs
βββ error.rs # Custom error types
Command Patterns
// commands/file.rs
use tauri::State;
use crate::{state::AppState, error::AppError};
#[tauri::command]
pub async fn read_file(
path: String,
state: State<'_, AppState>,
) -> Result<String, String> {
std::fs::read_to_string(&path)
.map_err(|e| format!("Failed to read file: {}", e))
}
#[tauri::command]
pub async fn save_file(
path: String,
content: String,
) -> Result<(), String> {
std::fs::write(&path, &content)
.map_err(|e| format!("Failed to save file: {}", e))
}
Error Handling
// error.rs
use serde::Serialize;
#[derive(Debug, Serialize)]
pub enum AppError {
Io(String),
Database(String),
Validation(String),
NotFound(String),
}
impl From<std::io::Error> for AppError {
fn from(err: std::io::Error) -> Self {
AppError::Io(err.to_string())
}
}
// Convert to Tauri invoke error
impl From<AppError> for tauri::ipc::InvokeError {
fn from(err: AppError) -> Self {
tauri::ipc::InvokeError::from(serde_json::to_string(&err).unwrap())
}
}
State Management
// state/mod.rs
use std::sync::Mutex;
use serde::{Deserialize, Serialize};
#[derive(Default)]
pub struct AppState {
pub settings: Mutex<AppSettings>,
pub cache: Mutex<Vec<String>>,
}
#[derive(Default, Serialize, Deserialize, Clone)]
pub struct AppSettings {
pub theme: String,
pub language: String,
}
// lib.rs - Register state
pub fn run() {
tauri::Builder::default()
.manage(AppState::default())
.invoke_handler(tauri::generate_handler![
commands::file::read_file,
commands::file::save_file,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Permissions & Security
Capabilities (Tauri 2.0)
Tauri 2.0 uses a capability-based security model. Define permissions in src-tauri/capabilities/:
// src-tauri/capabilities/default.json
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Default capabilities for the app",
"windows": ["main"],
"permissions": [
"core:default",
"fs:default",
"dialog:default",
"shell:allow-open"
]
}
Permission Scopes
// Fine-grained file system access
{
"permissions": [
{
"identifier": "fs:allow-read",
"allow": [
{ "path": "$APPDATA/**" },
{ "path": "$DOCUMENT/**" }
]
},
{
"identifier": "fs:allow-write",
"allow": [
{ "path": "$APPDATA/**" }
]
}
]
}
Security Best Practices
| Practice | Implementation |
|---|---|
| Minimal permissions | Only request what you need |
| Input validation | Validate all frontend data in Rust |
| Path traversal prevention | Use tauri::path APIs, not raw strings |
No dangerousRemoteDomainIpcAccess |
Avoid unless absolutely necessary |
| CSP headers | Configure in tauri.conf.json |
// tauri.conf.json - Security settings
{
"app": {
"security": {
"csp": "default-src 'self'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'"
}
}
}
Plugins
Official Plugins
Install via npm + Cargo:
# Dialog plugin
npm install @tauri-apps/plugin-dialog
cargo add tauri-plugin-dialog -F tauri-plugin-dialog/unstable
# File system plugin
npm install @tauri-apps/plugin-fs
cargo add tauri-plugin-fs
# Store plugin (persistent storage)
npm install @tauri-apps/plugin-store
cargo add tauri-plugin-store
Register Plugins
// lib.rs
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_store::Builder::default().build())
.invoke_handler(tauri::generate_handler![/* commands */])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Using Plugins in Frontend
// Dialog
import { open, save } from '@tauri-apps/plugin-dialog';
const filePath = await open({
multiple: false,
filters: [{ name: 'Text', extensions: ['txt', 'md'] }],
});
// File system
import { readTextFile, writeTextFile } from '@tauri-apps/plugin-fs';
const content = await readTextFile(filePath);
await writeTextFile(filePath, newContent);
// Store (persistent key-value)
import { Store } from '@tauri-apps/plugin-store';
const store = await Store.load('settings.json');
await store.set('theme', 'dark');
const theme = await store.get<string>('theme');
Custom Plugin
// plugins/mod.rs
use tauri::{
plugin::{Builder, TauriPlugin},
Runtime,
};
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("my-plugin")
.invoke_handler(tauri::generate_handler![plugin_command])
.build()
}
#[tauri::command]
fn plugin_command() -> String {
"Hello from plugin!".into()
}
Build & Distribution
Development
npm run tauri dev # Hot-reload development
npm run tauri dev -- --release # Test release build
Production Build
npm run tauri build # Build for current platform
Build Configuration
// tauri.conf.json
{
"productName": "My App",
"version": "1.0.0",
"identifier": "com.mycompany.myapp",
"build": {
"beforeBuildCommand": "npm run build",
"beforeDevCommand": "npm run dev",
"devUrl": "http://localhost:5173",
"frontendDist": "../dist"
},
"bundle": {
"active": true,
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/icon.icns",
"icons/icon.ico"
],
"macOS": {
"minimumSystemVersion": "10.13"
},
"windows": {
"certificateThumbprint": null,
"timestampUrl": ""
}
}
}
Platform-Specific Builds
# Cross-compile (requires toolchain)
npm run tauri build -- --target x86_64-pc-windows-msvc
npm run tauri build -- --target aarch64-apple-darwin
npm run tauri build -- --target x86_64-unknown-linux-gnu
Auto-Updater
npm install @tauri-apps/plugin-updater
cargo add tauri-plugin-updater
// lib.rs
.plugin(tauri_plugin_updater::Builder::default().build())
// capabilities/default.json
{
"permissions": ["updater:default"]
}
Frontend Architecture (MVVM)
Folder Structure
src/
βββ main.tsx # Entry point
βββ App.tsx # Router setup
βββ router/ # Route definitions
β βββ index.tsx
βββ views/ # View layer (pages)
β βββ Home/
β β βββ index.tsx
β β βββ HomeView.tsx
β βββ Settings/
β βββ index.tsx
βββ viewmodels/ # ViewModel layer (hooks)
β βββ useHomeViewModel.ts
β βββ useSettingsViewModel.ts
βββ models/ # Model layer (types)
β βββ index.ts
βββ stores/ # Zustand stores
β βββ useAppStore.ts
βββ services/ # Tauri bridge
β βββ tauriService.ts
βββ components/ # Reusable UI
β βββ Button/
βββ hooks/ # Custom hooks
βββ styles/
βββ globals.css
MVVM Pattern
| Layer | Responsibility | Example |
|---|---|---|
| Model | Data types, stores | models/, stores/ |
| View | UI rendering (dumb) | views/, components/ |
| ViewModel | Logic, state binding | viewmodels/ hooks |
Zustand Store
// stores/useAppStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
interface AppState {
theme: 'light' | 'dark';
sidebarOpen: boolean;
setTheme: (theme: 'light' | 'dark') => void;
toggleSidebar: () => void;
}
export const useAppStore = create<AppState>()(
persist(
(set) => ({
theme: 'dark',
sidebarOpen: true,
setTheme: (theme) => set({ theme }),
toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
}),
{
name: 'app-storage',
storage: createJSONStorage(() => localStorage),
}
)
);
Tauri Bridge Service
// services/tauriService.ts
import { invoke } from '@tauri-apps/api/core';
export const tauriService = {
async readFile(path: string): Promise<string> {
return invoke<string>('read_file', { path });
},
async saveFile(path: string, content: string): Promise<void> {
return invoke('save_file', { path, content });
},
async getSettings(): Promise<AppSettings> {
return invoke<AppSettings>('get_settings');
},
};
ViewModel Hook
// viewmodels/useHomeViewModel.ts
import { useState, useEffect, useCallback } from 'react';
import { tauriService } from '../services/tauriService';
import { useAppStore } from '../stores/useAppStore';
export function useHomeViewModel() {
const [files, setFiles] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const { theme } = useAppStore();
const loadFiles = useCallback(async () => {
setLoading(true);
setError(null);
try {
const data = await tauriService.listFiles();
setFiles(data);
} catch (e) {
setError(e instanceof Error ? e.message : 'Unknown error');
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
loadFiles();
}, [loadFiles]);
return { files, loading, error, theme, loadFiles };
}
View Component
// views/Home/HomeView.tsx
import { useHomeViewModel } from '../../viewmodels/useHomeViewModel';
export function HomeView() {
const { files, loading, error, loadFiles } = useHomeViewModel();
if (loading) return <div className="animate-pulse">Loading...</div>;
if (error) return <div className="text-error">{error}</div>;
return (
<div className="p-4">
<h1 className="text-2xl font-bold text-primary">Files</h1>
<ul className="mt-4 space-y-2">
{files.map((file) => (
<li key={file} className="p-2 bg-surface rounded">
{file}
</li>
))}
</ul>
<button onClick={loadFiles} className="mt-4 btn-primary">
Refresh
</button>
</div>
);
}
React Router Setup
// router/index.tsx
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { lazy, Suspense } from 'react';
const Home = lazy(() => import('../views/Home'));
const Settings = lazy(() => import('../views/Settings'));
const router = createBrowserRouter([
{ path: '/', element: <Home /> },
{ path: '/settings', element: <Settings /> },
]);
export function AppRouter() {
return (
<Suspense fallback={<div>Loading...</div>}>
<RouterProvider router={router} />
</Suspense>
);
}
Decision Tree
What do you need?
βββ Create new project
β βββ npm create tauri-app@latest -- --template react-ts
βββ Add Rust command
β βββ Create in commands/, register in lib.rs
βββ Add plugin
β βββ Official β npm install + cargo add
β βββ Custom β Create in plugins/
βββ Manage permissions
β βββ Edit capabilities/*.json
βββ Manage frontend state
β βββ Use Zustand stores/
βββ Call Rust from React
β βββ Use tauriService bridge
βββ Build for production
βββ npm run tauri build
Common Pitfalls
| Issue | Solution |
|---|---|
| Commands not found | Register in generate_handler![] |
| Permission denied | Add to capabilities/*.json |
| State not updating | Check Mutex lock is released |
| Build fails on CI | Install platform dependencies |
| Large bundle size | Enable strip and lto in Cargo.toml |
Resources
Examples
- Project Structure - Recommended folder layout
- Rust Backend:
- Commands - Command patterns
- State - State management
- Error Handling - Error types
- React Frontend:
- MVVM Structure - Architecture guide
- Zustand Patterns - Store patterns
- Tauri Hooks - Custom hooks
Templates
- tauri.conf.json - Config template
External Resources
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.