Tauri Patterns
Common Tauri 2.0 patterns: system tray apps, menu bar apps, file handling, SQLite database integration, IPC communication patterns, background tasks, and single-instance enforcement.
You are an expert in Tauri 2.0 application patterns, covering system tray integration, menu construction, file handling, database access, IPC design, background processing, and single-instance enforcement.
## Key Points
- **Storing database connections without Mutex**: SQLite connections are not thread-safe. Always wrap them in `Mutex<Connection>` when shared via Tauri state.
- **Granting blanket file system permissions**: Use scoped permissions in your capability files. Only allow access to the directories your app actually needs (app data, documents, etc.).
- **Using `unwrap()` in command handlers**: Commands should return `Result<T, String>` and use `map_err` to convert errors. Panicking in a command crashes the backend process.
- **Polling from the frontend instead of using events**: Do not use `setInterval` in JavaScript to check for updates. Use Rust events (`app.emit()`) to push data to the frontend when it changes.
- **Ignoring the single-instance plugin for document-based apps**: Without it, double-clicking a file associated with your app opens a new instance each time, which is unexpected for users.
## Quick Example
```toml
[dependencies]
tauri = { version = "2", features = ["tray-icon"] }
tauri-plugin-shell = "2"
```
```toml
# Cargo.toml
[dependencies]
tauri-plugin-dialog = "2"
tauri-plugin-fs = "2"
```skilldb get tauri-skills/Tauri PatternsFull skill: 594 linesTauri Patterns — Common Desktop Application Architectures
You are an expert in Tauri 2.0 application patterns, covering system tray integration, menu construction, file handling, database access, IPC design, background processing, and single-instance enforcement.
System Tray Apps
System tray (notification area) apps run in the background with an icon in the system tray. They may or may not have a visible window.
Cargo.toml
[dependencies]
tauri = { version = "2", features = ["tray-icon"] }
tauri-plugin-shell = "2"
Capability
// src-tauri/capabilities/tray.json
{
"identifier": "tray-capability",
"windows": ["main"],
"permissions": [
"core:tray:default",
"core:tray:allow-set-icon",
"core:tray:allow-set-tooltip",
"core:menu:default"
]
}
Rust Setup
use tauri::{
menu::{Menu, MenuItem},
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
Manager,
};
fn main() {
tauri::Builder::default()
.setup(|app| {
// Build the tray menu
let show = MenuItem::with_id(app, "show", "Show Window", true, None::<&str>)?;
let hide = MenuItem::with_id(app, "hide", "Hide Window", true, None::<&str>)?;
let quit = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&show, &hide, &quit])?;
// Create the tray icon
TrayIconBuilder::new()
.icon(app.default_window_icon().unwrap().clone())
.tooltip("My App")
.menu(&menu)
.on_menu_event(|app, event| match event.id.as_ref() {
"show" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
"hide" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.hide();
}
}
"quit" => {
app.exit(0);
}
_ => {}
})
.on_tray_icon_event(|tray, event| {
// Show window on left click
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(())
})
.run(tauri::generate_context!())
.expect("error while running application");
}
Hide to Tray on Close
use tauri::Manager;
fn main() {
tauri::Builder::default()
.setup(|app| {
let window = app.get_webview_window("main").unwrap();
// Intercept the close event -- hide instead of quitting
window.on_window_event(move |event| {
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
api.prevent_close();
// Hide the window instead of closing
// The app stays running in the tray
}
});
Ok(())
})
// ... tray setup as above
.run(tauri::generate_context!())
.expect("error");
}
Menu Bar Apps
Application menus (File, Edit, View, etc.) for desktop apps:
use tauri::menu::{Menu, MenuBuilder, MenuItemBuilder, SubmenuBuilder};
fn main() {
tauri::Builder::default()
.setup(|app| {
let file_menu = SubmenuBuilder::new(app, "File")
.item(&MenuItemBuilder::with_id("new", "New").accelerator("CmdOrCtrl+N").build(app)?)
.item(&MenuItemBuilder::with_id("open", "Open...").accelerator("CmdOrCtrl+O").build(app)?)
.item(&MenuItemBuilder::with_id("save", "Save").accelerator("CmdOrCtrl+S").build(app)?)
.separator()
.item(&MenuItemBuilder::with_id("quit", "Quit").accelerator("CmdOrCtrl+Q").build(app)?)
.build(app)?;
let edit_menu = SubmenuBuilder::new(app, "Edit")
.undo()
.redo()
.separator()
.cut()
.copy()
.paste()
.select_all()
.build(app)?;
let menu = MenuBuilder::new(app)
.item(&file_menu)
.item(&edit_menu)
.build(app)?;
app.set_menu(menu)?;
app.on_menu_event(|app, event| {
match event.id().as_ref() {
"new" => { /* handle new file */ }
"open" => { /* handle open */ }
"save" => { /* handle save */ }
"quit" => app.exit(0),
_ => {}
}
});
Ok(())
})
.run(tauri::generate_context!())
.expect("error");
}
File Handling
File Dialog and Read/Write
# Cargo.toml
[dependencies]
tauri-plugin-dialog = "2"
tauri-plugin-fs = "2"
// capabilities/files.json
{
"identifier": "file-capability",
"windows": ["main"],
"permissions": [
"dialog:default",
"dialog:allow-open",
"dialog:allow-save",
"fs:default",
"fs:allow-read",
"fs:allow-write"
]
}
use std::fs;
use tauri::command;
#[command]
fn read_file(path: String) -> Result<String, String> {
fs::read_to_string(&path).map_err(|e| e.to_string())
}
#[command]
fn write_file(path: String, content: String) -> Result<(), String> {
fs::write(&path, content).map_err(|e| e.to_string())
}
// Frontend
import { open, save } from "@tauri-apps/plugin-dialog";
import { invoke } from "@tauri-apps/api/core";
async function openFile() {
const path = await open({
filters: [
{ name: "Text", extensions: ["txt", "md"] },
{ name: "All Files", extensions: ["*"] },
],
multiple: false,
});
if (path) {
const content = await invoke<string>("read_file", { path });
editor.setValue(content);
}
}
async function saveFile() {
const path = await save({
filters: [{ name: "Text", extensions: ["txt"] }],
defaultPath: "untitled.txt",
});
if (path) {
const content = editor.getValue();
await invoke("write_file", { path, content });
}
}
Drag and Drop
use tauri::Manager;
fn main() {
tauri::Builder::default()
.setup(|app| {
let window = app.get_webview_window("main").unwrap();
window.on_drag_drop_event(|window, event| {
match event {
tauri::DragDropEvent::Drop { paths, position } => {
for path in paths {
println!("Dropped file: {:?}", path);
window.emit("file-dropped", path.to_string_lossy().to_string()).unwrap();
}
}
_ => {}
}
});
Ok(())
})
.run(tauri::generate_context!())
.expect("error");
}
SQLite Database
# Cargo.toml
[dependencies]
tauri-plugin-sql = { version = "2", features = ["sqlite"] }
Using the SQL Plugin (Frontend-Driven)
import Database from "@tauri-apps/plugin-sql";
// Open or create database (stored in app data directory)
const db = await Database.load("sqlite:app.db");
// Create tables
await db.execute(`
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// Insert
const result = await db.execute(
"INSERT INTO notes (title, content) VALUES ($1, $2)",
["My Note", "Note content here"]
);
console.log("Inserted ID:", result.lastInsertId);
// Query
const notes = await db.select<Array<{
id: number;
title: string;
content: string;
created_at: string;
}>>("SELECT * FROM notes ORDER BY updated_at DESC");
// Update
await db.execute(
"UPDATE notes SET content = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2",
["Updated content", 1]
);
// Delete
await db.execute("DELETE FROM notes WHERE id = $1", [1]);
Using Rusqlite (Rust-Driven)
# Cargo.toml
[dependencies]
rusqlite = { version = "0.31", features = ["bundled"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
use rusqlite::Connection;
use serde::{Deserialize, Serialize};
use std::sync::Mutex;
use tauri::State;
#[derive(Debug, Serialize, Deserialize)]
struct Note {
id: i64,
title: String,
content: String,
}
struct AppState {
db: Mutex<Connection>,
}
#[tauri::command]
fn get_notes(state: State<AppState>) -> Result<Vec<Note>, String> {
let db = state.db.lock().map_err(|e| e.to_string())?;
let mut stmt = db.prepare("SELECT id, title, content FROM notes")
.map_err(|e| e.to_string())?;
let notes = stmt.query_map([], |row| {
Ok(Note {
id: row.get(0)?,
title: row.get(1)?,
content: row.get(2)?,
})
})
.map_err(|e| e.to_string())?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| e.to_string())?;
Ok(notes)
}
fn main() {
let db = Connection::open("app.db").expect("Failed to open database");
db.execute_batch(
"CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT DEFAULT ''
)"
).expect("Failed to create table");
tauri::Builder::default()
.manage(AppState { db: Mutex::new(db) })
.invoke_handler(tauri::generate_handler![get_notes])
.run(tauri::generate_context!())
.expect("error");
}
IPC Patterns
Command Pattern (Frontend Calls Rust)
#[derive(serde::Serialize)]
struct User {
id: u32,
name: String,
email: String,
}
#[tauri::command]
async fn fetch_user(id: u32) -> Result<User, String> {
// Simulate async work
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
Ok(User {
id,
name: "Alice".to_string(),
email: "alice@example.com".to_string(),
})
}
// Register commands
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![fetch_user])
// Frontend
import { invoke } from "@tauri-apps/api/core";
interface User {
id: number;
name: string;
email: string;
}
const user = await invoke<User>("fetch_user", { id: 1 });
Event Pattern (Rust Pushes to Frontend)
use tauri::Emitter;
#[tauri::command]
async fn start_processing(app: tauri::AppHandle) -> Result<(), String> {
// Spawn a background task that emits progress events
tauri::async_runtime::spawn(async move {
for i in 0..=100 {
app.emit("progress", i).unwrap();
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
app.emit("complete", "Done!").unwrap();
});
Ok(())
}
import { listen } from "@tauri-apps/api/event";
import { invoke } from "@tauri-apps/api/core";
const unlisten = await listen<number>("progress", (event) => {
progressBar.value = event.payload;
});
const unlistenComplete = await listen<string>("complete", (event) => {
console.log(event.payload);
unlisten(); // clean up
unlistenComplete();
});
await invoke("start_processing");
Channel Pattern (Streaming Data)
use tauri::ipc::Channel;
#[tauri::command]
fn stream_logs(channel: Channel<String>) -> Result<(), String> {
std::thread::spawn(move || {
for line in read_log_lines() {
channel.send(line).unwrap();
}
});
Ok(())
}
Background Tasks
Long-Running Tasks with Tokio
use std::sync::Arc;
use tokio::sync::Mutex;
use tauri::{State, Emitter};
struct TaskManager {
running: Arc<Mutex<bool>>,
}
#[tauri::command]
async fn start_sync(
app: tauri::AppHandle,
state: State<'_, TaskManager>,
) -> Result<(), String> {
let running = state.running.clone();
// Check if already running
let mut is_running = running.lock().await;
if *is_running {
return Err("Sync already in progress".to_string());
}
*is_running = true;
drop(is_running);
tauri::async_runtime::spawn(async move {
loop {
let should_run = *running.lock().await;
if !should_run {
break;
}
// Do sync work
match perform_sync().await {
Ok(count) => {
app.emit("sync-progress", count).unwrap();
}
Err(e) => {
app.emit("sync-error", e.to_string()).unwrap();
}
}
tokio::time::sleep(std::time::Duration::from_secs(60)).await;
}
});
Ok(())
}
#[tauri::command]
async fn stop_sync(state: State<'_, TaskManager>) -> Result<(), String> {
let mut running = state.running.lock().await;
*running = false;
Ok(())
}
Single-Instance Enforcement
Prevent multiple instances of your app from running simultaneously:
# Cargo.toml
[dependencies]
tauri-plugin-single-instance = "2"
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_single_instance::init(|app, args, cwd| {
// This closure runs when a second instance is launched
// Focus the existing window instead
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
// You can also handle the args from the second instance
// e.g., open a file that was double-clicked
if args.len() > 1 {
if let Some(window) = app.get_webview_window("main") {
let _ = window.emit("open-file", &args[1]);
}
}
}))
.run(tauri::generate_context!())
.expect("error");
}
Anti-Patterns
- Performing heavy computation in IPC commands synchronously: Use
asynccommands withtauri::async_runtime::spawnfor CPU-intensive work. Synchronous commands block the IPC thread and freeze the UI. - Storing database connections without Mutex: SQLite connections are not thread-safe. Always wrap them in
Mutex<Connection>when shared via Tauri state. - Emitting events without cleanup on the frontend: Every
listen()call returns an unlisten function. Failing to call it causes memory leaks, especially in SPA frameworks where components mount and unmount. - Granting blanket file system permissions: Use scoped permissions in your capability files. Only allow access to the directories your app actually needs (app data, documents, etc.).
- Using
unwrap()in command handlers: Commands should returnResult<T, String>and usemap_errto convert errors. Panicking in a command crashes the backend process. - Polling from the frontend instead of using events: Do not use
setIntervalin JavaScript to check for updates. Use Rust events (app.emit()) to push data to the frontend when it changes. - Ignoring the single-instance plugin for document-based apps: Without it, double-clicking a file associated with your app opens a new instance each time, which is unexpected for users.
Install this skill directly: skilldb add tauri-skills
Related Skills
Tauri Commands
Rust commands with the invoke pattern, argument passing, return types, async commands, error handling, state management, and type safety between Rust and TypeScript in Tauri 2.0.
Tauri Distribution
Distributing Tauri applications including installers for MSI, DMG, AppImage, and deb, auto-update with the built-in updater, code signing for Windows and macOS, CI/CD builds, and cross-compilation.
Tauri Frontend
Frontend integration with Tauri 2.0 including React, Vue, Svelte, and Solid frameworks, Vite configuration, asset handling, window management, multiple windows, and webview communication.
Tauri Fundamentals
Tauri 2.0 architecture, Rust backend with webview frontend, project setup with Cargo and npm, development workflow, and build targets for Windows, macOS, Linux, iOS, and Android.
Tauri Mobile
Tauri 2.0 mobile development for iOS and Android, including platform-specific code, mobile plugins, testing on simulators and devices, and app store distribution.
Tauri Plugins
Tauri 2.0 plugin system including official plugins for filesystem, shell, dialog, notification, HTTP, clipboard, updater, and deep-link, plus community plugins and writing custom plugins.