Guide

Tauri fundamentals explained

Electron proved that web developers can ship desktop apps — but bundling an entire Chromium instance inflates installers to 150–250 MB and RAM usage to hundreds of megabytes for a simple utility. Tauri inverts the model: a Rust native core handles filesystem access, networking, and OS integration while the UI renders in the platform’s built-in webview (WebView2 on Windows, WKWebView on macOS, WebKitGTK on Linux). Your React, Vue, or Svelte frontend built with Vite talks to Rust through a typed IPC layer. Installers often land under 10 MB. This guide covers Tauri v2 architecture, project layout, invoke commands and events, the capabilities security model, plugins, bundling and auto-update, a Harbor Fleet offline cargo tracker worked example, a desktop framework decision table, common pitfalls, and a production checklist — complementing our Rust fundamentals guide and WebAssembly guide without repeating their language-level detail.

Architecture: Rust core and system webview

A Tauri app has two cooperating processes at runtime. The core (written in Rust) owns the application lifecycle, spawns windows, registers IPC handlers, and mediates every privileged operation. The webview loads your frontend assets — HTML, CSS, and JavaScript from a dev server during development or from embedded files in production. Communication crosses a narrow bridge: the frontend calls Rust via invoke, and Rust pushes updates via emit events.

Because Tauri does not ship Chromium, binary size and memory footprint track your actual code, not a browser engine. The trade-off is webview consistency: you test against each platform’s engine rather than one frozen Chromium build. For internal tools, developer utilities, and apps where disk and RAM matter on older laptops, that trade-off usually wins.

What lives where

  • Rust core — database access, background sync, file I/O, cryptography, hardware APIs, long-running tasks.
  • Web frontend — layout, forms, charts, routing, animations; anything you would build for the browser.
  • IPC boundary — serializable arguments and return values only; no raw pointers or shared memory across the bridge.

Project structure and tooling

Tauri v2 scaffolds with npm create tauri-app@latest or cargo install create-tauri-app --locked. A typical monorepo layout:

  • src/ — frontend source (React, Vue, Svelte, or plain HTML).
  • src-tauri/ — Rust crate with Cargo.toml, src/main.rs, src/lib.rs, and tauri.conf.json.
  • src-tauri/capabilities/ — JSON permission manifests per window (v2 security model).

Development runs two processes: npm run dev starts the Vite dev server, and cargo tauri dev launches the native shell pointed at localhost:1420 (configurable). Hot module replacement works on the frontend; Rust changes trigger a recompile. Production builds call cargo tauri build, which compiles Rust in release mode, bundles frontend assets, and produces platform installers (.msi, .dmg, .deb/.AppImage).

Commands, events, and state

Invoke: frontend to Rust

Define a Rust function with #[tauri::command], register it in the builder, and call it from JavaScript:

// src-tauri/src/lib.rs
#[tauri::command]
fn read_manifest(path: String) -> Result<String, String> {
    std::fs::read_to_string(path).map_err(|e| e.to_string())
}
// src/App.tsx
import { invoke } from '@tauri-apps/api/core';
const text = await invoke<string>('read_manifest', { path: '/data/manifest.json' });

Commands run on async-capable threads. Heavy work should use tauri::async_runtime::spawn or tokio tasks so the webview stays responsive. Return types must implement serde::Serialize; use Result<T, E> for explicit error propagation to the frontend.

Emit: Rust to frontend

Push real-time updates without polling: app.emit("sync-progress", payload) on the Rust side, listen('sync-progress', handler) in JavaScript. Useful for download progress, log streaming, and background job status.

Managed state

Share long-lived Rust state across commands with .manage(AppState::default()) and State<'_, AppState> in command signatures — database pools, HTTP clients, configuration caches. Do not store secrets in frontend localStorage; keep API keys and tokens in Rust memory only.

Capabilities and the security model

Tauri v2 replaces the older allowlist with capabilities: JSON files that declare exactly which windows may call which APIs. A window without the fs:allow-read permission cannot read files, even if a command tries. This defense-in-depth model matters because your frontend is untrusted input surface — XSS in the webview must not become arbitrary filesystem access.

  • Scope filesystem permissions to specific paths (e.g. $APPDATA/myapp/**).
  • Grant http:allow-fetch only to known API hosts, not wildcards.
  • Disable shell:allow-open unless you need external browser links.
  • Audit capabilities in code review the same way you audit API routes.

Custom commands inherit the invoking window’s capability set. If a command needs elevated access, validate inputs in Rust and reject paths outside allowed directories before calling std::fs.

Plugins, bundling, and distribution

Official plugins

Tauri ships plugins for common desktop needs: tauri-plugin-dialog (native file pickers), tauri-plugin-fs, tauri-plugin-http, tauri-plugin-shell, tauri-plugin-updater, and tauri-plugin-store (encrypted key-value persistence). Register plugins in lib.rs and enable matching capabilities.

Auto-update

The updater plugin checks a signed manifest hosted on your CDN or GitHub Releases. Updates download in the background and prompt the user to restart. Sign update artifacts with your private key; embed the public key in tauri.conf.json. Never skip signature verification in production.

Code signing

macOS requires Apple Developer ID signing and notarization for Gatekeeper acceptance. Windows SmartScreen trusts Authenticode-signed installers faster. Budget time and certificates before launch — unsigned builds work for internal distribution but trigger scary OS warnings for end users.

Worked example: Harbor Fleet offline cargo tracker

Harbor Fleet operators at remote ports need shipment status when satellite links drop. A Tauri desktop companion syncs when online and serves a local dashboard offline.

  1. Frontend — React + Vite table of containers, filterable by status and ETA; charts from a lightweight library.
  2. SQLite in Rustrusqlite connection in managed state; sync_from_api command fetches JSON, upserts rows inside a transaction.
  3. Background sync — Tokio interval task emits sync-complete events; frontend listens and refreshes without polling.
  4. Capabilitiesfs:scope limited to $APPDATA/harbor-fleet/**; http:scope allows only api.harbor-fleet.internal.
  5. Tray icon — minimize-to-tray with unread-alert badge via tauri-plugin-notification.
  6. Updater — signed releases pushed weekly; operators on metered links defer downloads until Wi-Fi.

Installer size: ~6 MB (vs ~180 MB for an Electron equivalent). Idle RAM: ~45 MB with the dashboard open. The team reused 80% of their existing React components from the web portal.

Framework decision table

Your situation Choose Tauri Consider instead
Small utility, installer size matters System webview keeps binaries tiny Electron if you need identical rendering everywhere
Team knows Rust or wants to learn Native performance and safety at the core Electron + Node if Rust hiring is impossible
Heavy filesystem and crypto work Rust ecosystem (ring, rusqlite, reqwest) without FFI Native Swift/Kotlin per platform for OS-first UX
Mobile iOS/Android required Tauri mobile is maturing but not default yet Capacitor or React Native for store distribution
Complex DevTools and Chrome extensions Not ideal — webview DevTools vary by OS Electron with full Chromium DevTools protocol
Existing web SPA to desktop-wrap quickly Reuse Vite build with minimal IPC layer PWA if install friction and offline are enough

Common pitfalls

  • Blocking the main thread in commands — large file reads or CPU work freeze the webview; spawn async tasks and emit progress events.
  • Over-broad capabilitiesfs:allow-read on ** negates the security model; scope to app directories only.
  • Assuming identical webview behavior — test CSS and JS on all three platforms; flexbox gaps and fetch CORS differ subtly.
  • Secrets in frontend bundles — API keys embedded in Vite env vars ship in the installer; keep credentials in Rust.
  • Missing serde on command types — custom structs need #[derive(Serialize, Deserialize)] or invoke fails at runtime.
  • Dev-only paths in production — hardcoding localhost:1420 in frontend logic breaks release builds; use Tauri’s isTauri() guard.
  • Skipping code signing — users abandon installs blocked by Gatekeeper or SmartScreen warnings.

Production checklist

  • Scaffold with official create-tauri-app template matching your frontend framework.
  • Define capabilities per window before writing privileged commands.
  • Wrap all filesystem and network access in Rust commands; frontend never touches paths directly.
  • Use Result<T, String> or structured error types; surface errors in UI toast components.
  • Run cargo tauri build on CI for Windows, macOS, and Linux matrix targets.
  • Test release builds on each OS — dev server proxy hides asset-path bugs.
  • Configure updater signatures and host manifests on a CDN with versioned releases.
  • Sign and notarize macOS builds; Authenticode-sign Windows installers.
  • Measure installer size and idle RAM; document vs Electron baseline for stakeholders.
  • Log Rust panics to a file in $APPDATA; never let panics crash silently in production.

Key takeaways

  • Tauri pairs a Rust native core with your web frontend, using OS webviews instead of bundled Chromium.
  • IPC flows through typed invoke commands and emit events; keep privileged work in Rust.
  • Capabilities in v2 enforce least-privilege access per window — design them before shipping.
  • Plugins cover dialogs, filesystem, HTTP, notifications, and signed auto-updates.
  • Tauri fits teams that want small installers, low memory use, and Rust-level systems access while reusing web UI skills.

Related reading