lunx.docs
DocsFrameworksTauri

Tauri Adapter

Build native desktop applications with Rust as the backend and any web framework as the frontend. Tauri apps are 10× smaller than Electron with native OS performance.

Tauri 2 · Rust Backend · Native OS
Rust backend

Your app logic runs as a native Rust binary — memory-safe, fast, and with direct OS API access.

Tiny bundles

A Tauri app with React frontend is typically 5–15MB. Electron equivalent: 150–200MB.

Security model

Fine-grained capability system: each window only gets the OS permissions it explicitly declares.

Cross-platform

Single Rust + web codebase. Builds for macOS, Linux, and Windows from a single CI workflow.

Commands

Rust functions decorated with #[tauri::command] are callable from JavaScript via invoke().

Events

Bidirectional event system between Rust and JS. Emit events from Rust and listen in React.

Setup

bash
# Prerequisites: Rust stable toolchain
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

npm create lunx-dev@latest my-tauri-app -- --framework tauri --ts
cd my-tauri-app && npm install && npm run dev

Project structure

text
my-tauri-app/
├── src/                     # React/Vue/Svelte frontend
   ├── main.tsx
   └── App.tsx
├── src-tauri/               # Rust backend
   ├── src/
      ├── main.rs          # Tauri app entry
      ├── lib.rs           # Commands and event handlers
      └── db.rs            # SQLite database (optional)
   ├── Cargo.toml
   └── tauri.conf.json      # App metadata and permissions
├── lunx.config.ts
└── package.json

Rust commands

rustsrc-tauri/src/lib.rs
use tauri::State;
use std::sync::Mutex;

pub struct AppState {
    pub db_path: Mutex<String>,
}

// Commands are callable from JavaScript via invoke()
#[tauri::command]
async fn read_file(path: String) -> Result<String, String> {
    std::fs::read_to_string(&path)
        .map_err(|e| e.to_string())
}

#[tauri::command]
async fn greet(name: &str) -> String {
    format!("Hello, {}! Built with Tauri + Lunx.", name)
}

#[tauri::command]
async fn get_system_info() -> serde_json::Value {
    serde_json::json!({
        "os":     std::env::consts::OS,
        "arch":   std::env::consts::ARCH,
        "cores":  num_cpus::get(),
    })
}

pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![
            read_file,
            greet,
            get_system_info,
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Calling Rust from JavaScript

typescriptsrc/App.tsx
import { invoke }  from '@tauri-apps/api/tauri'
import { open }    from '@tauri-apps/api/dialog'
import { useState } from 'react'

export default function App() {
  const [greeting, setGreeting] = useState('')
  const [fileContent, setFileContent] = useState('')

  async function greet() {
    // Calls the Rust greet() command
    const result = await invoke<string>('greet', { name: 'Lunx' })
    setGreeting(result)
  }

  async function openFile() {
    // Opens native OS file picker dialog
    const path = await open({ multiple: false, filters: [{ name: 'Text', extensions: ['txt', 'md'] }] })
    if (typeof path === 'string') {
      const content = await invoke<string>('read_file', { path })
      setFileContent(content)
    }
  }

  return (
    <div className="p-8">
      <button onClick={greet}>Greet with Rust</button>
      <p>{greeting}</p>
      <button onClick={openFile}>Open File</button>
      <pre>{fileContent}</pre>
    </div>
  )
}
TypeScript types from Rust
Lunx auto-generates TypeScript types from your Rust command signatures using tauri-specta. The invoke() calls are fully typed — catch mismatches at compile time before they reach the user.