Prerequisites
Linux (Debian/Ubuntu):
sudo apt install build-essential curl wget file libssl-dev \
libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev \
libwebkit2gtk-4.1-dev
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.cargo/env
macOS:
xcode-select --install
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Windows:
- Install the latest Microsoft C++ Build Tools.
- Install rustup.
- WebView2 runtime ships with Windows 11; on Windows 10, the installer adds it automatically.
Plus Node.js (or Bun — see that tutorial) for the front-end side.
Create a project
cargo install create-tauri-app --locked
cargo create-tauri-app
# — or —
bun create tauri-app
The prompts pick a name, front-end framework (vanilla / React / Vue / Svelte / Solid / Qwik / Yew / Leptos / Sycamore), and package manager. The result is a directory with this shape:
my-app/
src/ <-- frontend (HTML/CSS/JS, or framework-managed)
src-tauri/
Cargo.toml <-- Rust crate
tauri.conf.json <-- Tauri config: window, build, identifiers
src/lib.rs <-- Rust entry
capabilities/ <-- IPC capability scopes
package.json
vite.config.ts <-- (if Vite-based front-end)
Run it
bun install # or npm/yarn/pnpm install
bun tauri dev # or: npm run tauri dev
The first run takes a while — cargo compiles the Tauri runtime and any plugins. Subsequent runs are incremental and fast. The front-end is served by Vite's dev server in development; the Tauri shell points the native WebView at it. Hot-reload on the front-end works exactly as in a browser; changes to Rust trigger a recompile.
The Rust side: commands
The bridge between front-end and Rust is "commands" — Rust functions decorated with #[tauri::command] that the front-end calls via invoke():
// src-tauri/src/lib.rs
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
#[tauri::command]
async fn read_file(path: String) -> Result<String, String> {
tokio::fs::read_to_string(&path)
.await
.map_err(|e| e.to_string())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet, read_file])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
On the front-end:
import { invoke } from "@tauri-apps/api/core";
const message: string = await invoke("greet", { name: "Tauri" });
console.log(message);
const contents: string = await invoke("read_file", {
path: "/home/amir/notes.md",
});
Commands serialize arguments and return values as JSON (with serde on the Rust side). Anything Serialize/Deserialize works, including custom structs.
Capabilities (the security model that's new in Tauri 2)
By default, the front-end can only call commands that have been explicitly authorized via a capability file. Capabilities are JSON documents under src-tauri/capabilities/:
// src-tauri/capabilities/default.json
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Default capabilities for desktop builds",
"windows": ["main"],
"permissions": [
"core:default",
"shell:allow-open",
{
"identifier": "fs:scope-read",
"allow": [{ "path": "$HOME/notes/**" }]
}
]
}
This is the part that makes Tauri's claim of "least-privilege webview" real: a Tauri app's WebView is sandboxed away from disk/network/process access by default; the capability file is the single place where those privileges are granted. The front-end cannot ask for fs:scope-read against $HOME/notes/** if the capability doesn't grant it.
Built-in plugins
Tauri ships plugins for the things every app needs:
tauri-plugin-fs— scoped filesystem accesstauri-plugin-dialog— native Open/Save dialogstauri-plugin-shell— spawn external commandstauri-plugin-http— HTTP client (separately scoped from the WebView's own fetch)tauri-plugin-store— persistent JSON key-valuetauri-plugin-notification— native OS notificationstauri-plugin-updater— in-app update (signed)
Add via:
cargo tauri add fs
cargo tauri add dialog
cargo tauri add http
Each cargo tauri add also registers the plugin in tauri.conf.json and pulls in matching capability shorthand permissions.
Build for production
bun tauri build
Output artifacts:
- Linux:
.deb,.rpm, AppImage (undersrc-tauri/target/release/bundle/) - macOS:
.dmgand.app - Windows:
.msiand.exeNSIS installer
Typical sizes: 3–10 MB for the binary, 4–15 MB for an installer with the bundled WebView2 bootstrap on Windows. Compare to Electron's ~100–150 MB baseline.
Code signing
Without signing, Windows shows SmartScreen warnings; macOS refuses to run unsigned apps from the internet. Two ways:
- Windows: get an OV or EV code-signing certificate (DigiCert, SSL.com, etc.). Set
TAURI_SIGNING_PFX_FILEandTAURI_SIGNING_PFX_PASSWORDin the env beforetauri build, or configuretauri.conf.json→bundle.windows.certificateThumbprintto point at a cert in the Windows cert store. - macOS: enroll in the Apple Developer Program ($99/year), get a Developer ID certificate, set
APPLE_CERTIFICATE,APPLE_CERTIFICATE_PASSWORD,APPLE_SIGNING_IDENTITY,APPLE_ID, andAPPLE_PASSWORD(app-specific) in env.tauri buildhandles both signing and notarization.
Mobile targets
Tauri 2 supports Android and iOS as first-class targets:
cargo tauri android init
cargo tauri android dev # run on emulator/device
cargo tauri ios init # macOS only
cargo tauri ios dev
The same Rust code and front-end run on both desktop and mobile, with platform-specific capability files. Mobile builds use the system WebView (Android System WebView on Android, WKWebView on iOS) — same engine model as desktop.
What Tauri is not
Tauri does not bundle a JS runtime. JS runs in the OS's WebView, which means engine version varies across user machines (especially WebKitGTK on Linux). Cutting-edge JS features sometimes need a polyfill or a build-time downlevel. Same idea as targeting old browsers — the front-end build pipeline (Vite/Webpack/esbuild) handles it. For applications that need a guaranteed-recent V8 (e.g. video editing with WebCodecs from before they shipped in all WebViews), Electron is still the safer choice.