---
description: Tauri 2 Rust backend conventions
globs: ["src-tauri/**/*.rs","src-tauri/capabilities/*.json","src-tauri/tauri.conf.json"]
alwaysApply: false
---

# Tauri 2 (Rust side)

- Entry in `src-tauri/src/lib.rs` (`pub fn run()`), with `main.rs` calling it so mobile targets work.
- Builder pattern: `.plugin(...)` for each plugin, `.manage(AppState::default())`, `.invoke_handler(tauri::generate_handler![...])`, `.setup(|app| {...})`.
- Commands: `#[tauri::command] async fn save_note(state: State<'_, AppState>, note: NoteInput) -> Result<Note, AppError>`. Arguments are camelCase on the JS side by default.
- `AppError` uses `thiserror` and implements `serde::Serialize` (serialize to a string or `{ kind, message }`) so the frontend gets a clean rejection.
- State uses `std::sync::Mutex` for short critical sections or `tokio::sync::Mutex` if held across `.await`.
- Emit progress with `app.emit("export-progress", payload)`; stream large data with `tauri::ipc::Channel` instead of huge return values.
- Every plugin or command a window can call must be allowed in `src-tauri/capabilities/<window>.json`; custom commands get permissions via `build.rs`/`permissions/` when using app-level permissions. Keep scopes narrow (e.g. `$APPDATA/**`).
- File paths from the frontend are untrusted: resolve against `app.path().app_data_dir()` and reject traversal.
- No `unwrap()` in commands; use `?`.

## Done when
`cargo fmt --check`, `cargo clippy -- -D warnings`, and `cargo test` pass in `src-tauri/`.
