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 withCargo.toml,src/main.rs,src/lib.rs, andtauri.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-fetchonly to known API hosts, not wildcards. - Disable
shell:allow-openunless 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.
- Frontend — React + Vite table of containers, filterable by status and ETA; charts from a lightweight library.
- SQLite in Rust —
rusqliteconnection in managed state;sync_from_apicommand fetches JSON, upserts rows inside a transaction. - Background sync — Tokio interval task emits
sync-completeevents; frontend listens and refreshes without polling. - Capabilities —
fs:scopelimited to$APPDATA/harbor-fleet/**;http:scopeallows onlyapi.harbor-fleet.internal. - Tray icon — minimize-to-tray with unread-alert badge via
tauri-plugin-notification. - 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 capabilities —
fs:allow-readon**negates the security model; scope to app directories only. - Assuming identical webview behavior — test CSS and JS on all three platforms; flexbox gaps and
fetchCORS 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:1420in frontend logic breaks release builds; use Tauri’sisTauri()guard. - Skipping code signing — users abandon installs blocked by Gatekeeper or SmartScreen warnings.
Production checklist
- Scaffold with official
create-tauri-apptemplate 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 buildon 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
invokecommands andemitevents; 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
- Rust fundamentals explained — ownership, Cargo, and async patterns the Tauri core uses
- Vite fundamentals explained — the typical frontend build tool in Tauri projects
- WebAssembly (WASM) explained — compile Rust to WASM for browser targets alongside desktop
- Swift fundamentals explained — when native macOS/iOS beats cross-platform webview wrappers