patrickhaahr-tauri
Tauri v2 Best Practices Guide
Context: Modern Tauri v2 development for cross-platform desktop and mobile applications
1. Security: Capability-Based Permission Model
Practice: Define granular capabilities in src-tauri/capabilities/ instead of using wildcard permissions.
Rationale: Tauri v2 implements the principle of least privilege through explicit capability boundaries. Each capability creates a security sandbox between frontend and backend, preventing compromised frontend code from accessing unintended system resources.
Implementation:
// src-tauri/capabilities/main.json
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main-capability",
"description": "Core capability for main window operations",
"windows": ["main"],
"permissions": [
"core:default",
"core:path:default",
{
"identifier": "fs:scope-app-recursive",
"allow": [{"path": "$APPDATA/*"}]
}
]
}
Key Points:
- Always include
$schemafor IDE validation - Use descriptive
identifieranddescriptionfields - Scope filesystem access to specific directories
- Reference capabilities in
tauri.conf.jsonunderapp.security.capabilities
2. Plugin Architecture: Modular Design
Practice: Treat all functionality as plugins. Use tauri add <plugin> for dependency management.
Rationale: Plugin-based architecture reduces compile times through selective compilation and makes dependencies explicit. The binary only includes functionality that is actually used.
Commands:
bun tauri add fs
bun tauri add shell
bun tauri add dialog
bun tauri add notification
Rust initialization:
// src-tauri/src/lib.rs
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_dialog::init())
.invoke_handler(tauri::generate_handler![my_custom_command])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Cargo.toml configuration:
[dependencies]
tauri-plugin-fs = "2.0"
tauri-plugin-shell = "2.0"
tauri-plugin-dialog = "2.0"
# or from Git
tauri-plugin-fs = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
3. Build Optimization: Cargo Profile Configuration
Practice: Configure release profile for optimal size and speed balance.
Rationale: Aggressive optimization during compilation produces smaller, faster binaries. The compile time investment is amortized over many builds.
Configuration:
# src-tauri/Cargo.toml
[profile.release]
codegen-units = 1
lto = "fat"
opt-level = "z"
panic = "abort"
strip = true
[profile.dev]
debug = true
Bundle optimization in tauri.conf.json:
{
"build": {
"removeUnusedCommands": true,
"beforeBuildCommand": "",
"beforeDevCommand": ""
}
}
4. Memory Management: Backpressure via Pagination
Practice: Stream large datasets with pagination instead of monolithic JSON payloads.
Rationale: Prevents IPC serialization overhead from dominating memory usage. Frontend requests data at consumption rate, applying natural backpressure.
Implementation:
#[tauri::command]
async fn get_directory_entries(
path: String,
offset: usize,
limit: usize
) -> Result<DirectoryPage, String> {
let entries: Vec<_> = std::fs::read_dir(path)?
.skip(offset)
.take(limit)
.collect::<Result<Vec<_>, _>>()?;
Ok(DirectoryPage {
entries: entries.into_iter().map(EntryInfo::from).collect(),
has_more: entries.len() == limit,
total_count: /* calculate if needed */
})
}
5. Async Runtime: CPU Work Isolation
Practice: Use tokio::task::spawn_blocking for CPU-intensive operations.
Rationale: Prevents blocking the tokio event loop. Maintains command responsiveness by isolating CPU work to a separate thread pool.
Implementation:
#[tauri::command]
async fn process_large_file(path: String) -> Result<String, String> {
let handle = tokio::task::spawn_blocking(move || {
let content = std::fs::read_to_string(path)?;
// CPU-intensive processing here
Ok::<_, String>(process_content(content))
});
handle.await.map_err(|e| e.to_string())?
}
6. Cross-Platform: Conditional Compilation
Practice: Abstract platform differences early using cfg attributes.
Rationale: System WebViews differ significantly (WebKitGTK, WKWebView, WebView2). Early abstraction prevents technical debt and ensures consistent behavior across platforms.
Implementation:
#[cfg(target_os = "linux")]
fn configure_webview() {
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
}
#[cfg(target_os = "windows")]
fn configure_webview() {
std::env::set_var("WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS",
"--disable-features=msWebOOUI");
}
#[cfg(target_os = "macos")]
fn configure_webview() {
// macOS-specific optimizations if needed
}
Platform-specific capabilities:
// src-tauri/capabilities/desktop.json
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "desktop-capability",
"platforms": ["linux", "macos", "windows"],
"windows": ["main"],
"permissions": ["global-shortcut:allow-register"]
}
7. Development Workflow: Tool Integration
Practice: Use consistent script commands and configure editor integration.
Rationale: Consistent commands across the development environment improve productivity. Modern package managers like bun offer speed and strict validation.
package.json:
{
"scripts": {
"dev": "tauri dev",
"build": "tauri build",
"lint": "bun eslint .",
"format": "prettier --write ."
}
}
Trust Boundaries Configuration:
// src-tauri/tauri.conf.json
{
"app": {
"security": {
"csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'",
"capabilities": ["main-capability"]
}
}
}
8. Command Registration and Scope Control
Practice: Explicitly register and scope custom commands.
Rationale: By default, all registered commands are accessible to all windows. For finer control, use AppManifest::commands in build.rs.
Implementation:
// src-tauri/build.rs
fn main() {
tauri_build::try_build(
tauri_build::Attributes::new()
.app_manifest(tauri_build::AppManifest::new().commands(&["safe_command"])),
)
.unwrap();
}
Isolation pattern for high-security scenarios:
// src-tauri/tauri.conf.json
{
"build": {
"frontendDist": "../dist",
"devPath": "http://localhost:1420"
},
"app": {
"security": {
"pattern": {
"use": "isolation",
"options": {
"dir": "../dist-isolation"
}
}
}
}
}
9. Remote URL Access Control
Practice: Explicitly configure remote URL access when needed.
Rationale: By default, the Tauri API is only accessible to bundled code. Remote sources require explicit capability configuration.
Implementation:
// src-tauri/capabilities/remote.json
{
"$schema": "../gen/schemas/remote-schema.json",
"identifier": "remote-capability",
"description": "Allow remote development sources",
"windows": ["main"],
"remote": {
"urls": ["https://*.localhost", "http://localhost:*"]
},
"permissions": ["core:default"]
}
Security Note: On Linux and Android, Tauri cannot distinguish between iframe requests and window requests. Use this feature cautiously.
10. Content Security Policy (CSP)
Practice: Define strict CSP in tauri.conf.json.
Rationale: CSP provides an additional security layer against XSS attacks and unauthorized script execution.
Configuration:
{
"app": {
"security": {
"csp": "default-src 'self'; connect-src 'self' https://api.example.com; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;"
}
}
}
11. Window-Specific Capabilities
Practice: Assign different capabilities to different windows.
Rationale: Reduce attack surface by giving each window only the permissions it needs.
Implementation:
// src-tauri/capabilities/admin.json
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "admin-capability",
"description": "Admin window with filesystem access",
"windows": ["admin-panel"],
"permissions": ["core:default", "fs:default", "shell:allow-open"]
}
// src-tauri/capabilities/user.json
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "user-capability",
"description": "Regular user window with limited access",
"windows": ["main", "settings"],
"permissions": ["core:default", "dialog:default"]
}
12. Error Handling and Validation
Practice: Validate all inputs and return meaningful error messages.
Rationale: Frontend code should receive actionable error information while hiding implementation details.
Implementation:
#[tauri::command]
async fn read_config(path: String) -> Result<Config, String> {
// Validate path
if path.is_empty() {
return Err("Path cannot be empty".to_string());
}
// Check file exists
if !std::path::Path::new(&path).exists() {
return Err(format!("File not found: {}", path));
}
// Read and parse
std::fs::read_to_string(path)
.map_err(|e| format!("Failed to read file: {}", e))?
.parse()
.map_err(|e| format!("Failed to parse config: {}", e))
}
13. State Management
Practice: Use Tauri state for managing application-wide resources.
Rationale: State provides a clean way to share resources across commands and manage application lifecycle.
Implementation:
struct AppState {
db: Mutex<Database>,
config: RwLock<AppConfig>,
}
#[tauri::command]
async fn get_config(state: tauri::State<AppState>) -> Result<AppConfig, String> {
Ok(*state.config.read().map_err(|e| e.to_string())?)
}
#[tauri::command]
async fn update_config(
new_config: AppConfig,
state: tauri::State<AppState>,
) -> Result<(), String> {
let mut config = state.config.write().map_err(|e| e.to_string())?;
*config = new_config;
Ok(())
}
fn main() {
let state = AppState {
db: Mutex::new(Database::new()?),
config: RwLock::new(AppConfig::default()),
};
tauri::Builder::default()
.manage(state)
.invoke_handler(tauri::generate_handler![get_config, update_config])
.run(tauri::generate_context!())
.expect("error running app");
}
14. Performance Monitoring
Practice: Implement IPC performance tracking for critical paths.
Rationale: Identify bottlenecks in the IPC layer before they become user-facing issues.
Implementation:
use std::time::Instant;
#[tauri::command]
async fn expensive_operation(input: String) -> Result<String, String> {
let start = Instant::now();
// Perform operation
let result = process_input(input)?;
// Log timing (consider making this conditional)
let elapsed = start.elapsed();
if elapsed > Duration::from_millis(100) {
println!("Warning: expensive_operation took {:?}", elapsed);
}
Ok(result)
}
Core Principle: Explicit Contracts
Tauri v2 enforces explicit contracts between frontend and backend. Every command, permission, and plugin must be declared. This verbosity is Rust's safety model applied to the web/desktop boundary.
Engineering Question: "What is the minimum API surface does my frontend require?"
This principle underlies all best practices and ensures:
- Minimal attack surface
- Clear trust boundaries
- Explicit dependencies
- Predictable behavior
Quick Reference
Directory Structure
tauri-app/
├── src/ # Frontend source
├── src-tauri/
│ ├── capabilities/ # Security permissions
│ │ ├── main.json
│ │ ├── desktop.json
│ │ └── mobile.json
│ ├── src/ # Rust backend
│ ├── Cargo.toml
│ └── tauri.conf.json
├── package.json
└── README.md
Essential Commands
bun tauri add fs shell dialog notification
bun tauri dev
bun tauri build
cargo clippy -- -D warnings
Security Checklist
- All capabilities include
$schema - Filesystem permissions are scoped (not default)
- CSP is configured
- Remote URLs are explicitly allowed if needed
- Windows have minimal required permissions
- Input validation on all commands
- Error messages don't leak sensitive information