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 access
  • tauri-plugin-dialog — native Open/Save dialogs
  • tauri-plugin-shell — spawn external commands
  • tauri-plugin-http — HTTP client (separately scoped from the WebView's own fetch)
  • tauri-plugin-store — persistent JSON key-value
  • tauri-plugin-notification — native OS notifications
  • tauri-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 (under src-tauri/target/release/bundle/)
  • macOS: .dmg and .app
  • Windows: .msi and .exe NSIS 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_FILE and TAURI_SIGNING_PFX_PASSWORD in the env before tauri build, or configure tauri.conf.jsonbundle.windows.certificateThumbprint to 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, and APPLE_PASSWORD (app-specific) in env. tauri build handles 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.