Tauri is a framework for building desktop applications using web technologies for the UI and Rust for the backend. Compared to Electron, Tauri apps are dramatically smaller — a Tauri app ships a few megabytes rather than a hundred megabytes, because it uses the operating system’s built-in WebView rather than bundling a full Chromium browser. For a local AI desktop application backed by Ollama, Tauri is an excellent choice: you get a native desktop app with a polished web-based UI, system tray integration, file system access, and native notifications — all without the weight of Electron.
Tauri’s Rust backend can call Ollama directly over HTTP using the reqwest crate, and expose the results to the JavaScript frontend through Tauri’s command system. The JavaScript frontend can be any web framework — React, Vue, Svelte, or plain JavaScript. This guide uses Svelte for the UI and Rust with reqwest for the Ollama integration.
Setup
Install Tauri prerequisites: Rust (via rustup), Node.js, and on Linux the required WebView2 dependencies. Then create a new project:
npm create tauri-app@latest ollama-desktop cd ollama-desktop npm install npm run tauri dev
Choose Svelte as the frontend framework. The dev command starts both the Vite dev server for the frontend and the Rust backend, opening a native desktop window showing your web UI. Add reqwest to the Rust dependencies in src-tauri/Cargo.toml:
[dependencies]
tauri = { version = "2", features = [] }
reqwest = { version = "0.12", features = ["json", "stream"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Defining Tauri Commands
Tauri commands are Rust functions exposed to the JavaScript frontend. Add the Ollama chat command in src-tauri/src/main.rs:
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone)]
struct Message {
role: String,
content: String,
}
#[tauri::command]
async fn chat(messages: Vec<Message>, model: String) -> Result<String, String> {
let client = reqwest::Client::new();
let resp = client
.post("http://localhost:11434/api/chat")
.json(&serde_json::json!({
"model": model,
"messages": messages,
"stream": false
}))
.timeout(std::time::Duration::from_secs(120))
.send()
.await
.map_err(|e| e.to_string())?;
let data: serde_json::Value = resp.json().await.map_err(|e| e.to_string())?;
Ok(data["message"]["content"].as_str().unwrap_or("").to_string())
}
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![chat])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}The #[tauri::command] attribute and invoke_handler registration wire the Rust function to the JavaScript frontend. Return types use Rust’s Result — Ok maps to a resolved JavaScript promise and Err maps to a rejected one. The async keyword makes the command non-blocking so the UI stays responsive while waiting for Ollama.
Calling Commands from JavaScript
Call the Rust command from the Svelte frontend using Tauri’s invoke function:
<script>
import { invoke } from "@tauri-apps/api/core";
let messages = [];
let input = "";
let loading = false;
async function send() {
if (!input.trim() || loading) return;
messages = [...messages, { role: "user", content: input }];
input = "";
loading = true;
try {
const reply = await invoke("chat", {
messages,
model: "llama3.2"
});
messages = [...messages, { role: "assistant", content: reply }];
} catch (err) {
messages = [...messages, { role: "assistant", content: `Error: ${err}` }];
} finally {
loading = false;
}
}
</script>The invoke function returns a Promise that resolves with the Rust function’s return value or rejects with the error string. The argument object’s keys map to the Rust function’s parameter names — Tauri handles the serialisation between JavaScript and Rust automatically using serde.
Streaming with Tauri Events
For streaming responses, use Tauri’s event system to emit tokens from Rust to JavaScript as they arrive:
use tauri::Manager;
use futures_util::StreamExt;
#[tauri::command]
async fn chat_stream(window: tauri::Window, messages: Vec<Message>, model: String) -> Result<(), String> {
let client = reqwest::Client::new();
let resp = client
.post("http://localhost:11434/api/chat")
.json(&serde_json::json!({ "model": model, "messages": messages, "stream": true }))
.send().await.map_err(|e| e.to_string())?;
let mut stream = resp.bytes_stream();
let mut buf = String::new();
while let Some(chunk) = stream.next().await {
let chunk = chunk.map_err(|e| e.to_string())?;
buf.push_str(&String::from_utf8_lossy(&chunk));
while let Some(pos) = buf.find('\n') {
let line = buf[..pos].trim().to_string();
buf = buf[pos+1..].to_string();
if line.is_empty() { continue; }
if let Ok(data) = serde_json::from_str::<serde_json::Value>(&line) {
let token = data["message"]["content"].as_str().unwrap_or("").to_string();
if !token.is_empty() {
window.emit("ollama-token", token).ok();
}
if data["done"].as_bool().unwrap_or(false) { break; }
}
}
}
window.emit("ollama-done", ()).ok();
Ok(())
}In the JavaScript frontend, listen for the events with listen("ollama-token", handler) from @tauri-apps/api/event. Append each token to the current assistant message in the handler. Listen for ollama-done to know when streaming is complete. Remember to unlisten from events when the component is destroyed to prevent memory leaks.
Native Features: System Tray and Notifications
One of Tauri’s main advantages over a browser-based app is access to native OS features. Add a system tray icon that lets users quickly open the chat window from anywhere on their desktop. Configure the tray in tauri.conf.json under the tray key, pointing at an icon file. In Rust, handle tray click events to show or hide the main window. This gives users a chat-with-AI-anywhere experience — click the tray icon, type a question, get an answer, close the window, all without switching away from whatever they are working on.
Native notifications via window.emit and the Tauri notification plugin let the app alert users when a long-running Ollama response finishes — useful when you have sent a complex request that will take 30+ seconds and gone back to other work while waiting. The notification appears as a native OS notification with the first line of the response, and clicking it brings the app window to the foreground.
File System Access
Tauri provides controlled access to the file system through its plugin system. Use the file system plugin to let users select documents, images, or code files to include as context for Ollama queries. The file picker opens a native OS file dialog, reads the selected file’s content, and passes it to the Rust backend alongside the user’s message. This makes Tauri the natural choice for a desktop application that needs to work with local files — PDFs, code files, logs, data files — as part of an AI workflow, without the browser security restrictions that prevent web apps from accessing the file system directly.
Tauri’s permission system requires you to declare which file system paths the application can access in the configuration, giving users clear control over what the app can and cannot read. Unlike Electron where the entire Node.js file system API is available, Tauri requires explicit permission declarations for each capability, which is both a security improvement and a useful forcing function for thinking clearly about what your application actually needs.
—Building and Distributing
Build the application for distribution with npm run tauri build. Tauri generates native installers for the current platform — an .exe installer on Windows, a .dmg on macOS, and .deb and .AppImage on Linux. The resulting installer is typically 5 to 15 megabytes, compared to 100+ megabytes for an equivalent Electron app. Distribute it directly to users who can install it like any other native application. Since Ollama must also be installed and running, include clear setup instructions with the installer that guide users through installing Ollama and pulling the required models before launching the desktop app.
Why Tauri Over a Browser Tab
The most common alternative to a Tauri desktop app for Ollama is simply opening a browser and pointing it at a locally served web application — Open WebUI, a Streamlit app, or a custom Flask endpoint. This works perfectly well for most use cases, and if a browser-based interface meets your needs, there is no reason to build a desktop app. Tauri earns its place in specific situations: when you want deep integration with the operating system (file system access, system tray, native notifications, keyboard shortcuts that work even when the app is not focused), when you want to distribute a polished single installer to non-technical users rather than asking them to run a server and open a browser, or when you want the app to launch in under a second without a visible browser window.
For a personal AI assistant that you use dozens of times per day, the quality-of-life difference between a native desktop app and a browser tab is real. A native app can be launched with a global keyboard shortcut, brought to the foreground from the system tray with a single click, and closed back to tray without losing conversation history. It feels like a tool that is always available rather than a website you visit. For users who are deeply invested in their workflow and want AI assistance to be as frictionless as possible, Tauri provides that native integration that browser-based tools cannot match.
Tauri vs Electron for Ollama Apps
Electron is the other major framework for building desktop apps with web technologies, and it powered the first generation of developer tools like VS Code, Slack, and Discord. Tauri is the modern alternative, and for Ollama applications it has clear advantages. The most visible difference is application size — a Tauri app installer is 5-15MB, while an equivalent Electron app is 100-200MB, because Electron bundles a full Chromium browser while Tauri uses the OS WebView. For an AI assistant app that users will install alongside Ollama itself (another large download), keeping the installer small reduces friction.
Performance is another meaningful difference. Tauri’s Rust backend is significantly faster than Electron’s Node.js backend for CPU-bound tasks, though for an Ollama integration where the bottleneck is LLM inference rather than backend processing, this matters less. Memory usage is lower in Tauri because there is no full browser process — the WebView shares system-level browser infrastructure already loaded by the OS. On a machine where Ollama is loading a 7B model into GPU memory, every megabyte of RAM saved by using Tauri over Electron for the frontend is a small but real contribution to keeping the system responsive.
The main reason to choose Electron over Tauri is if your team does not know Rust. Tauri requires writing backend logic in Rust, which has a steeper learning curve than Node.js. For teams with existing Rust experience or those willing to invest in learning it, Tauri is the better choice for new applications. For teams that need to ship quickly and are comfortable with Node.js, Electron remains a practical option even with its size and performance disadvantages.
Auto-Starting with Ollama
For the best user experience, configure the Tauri app to check whether Ollama is running on startup and offer to start it automatically if not. Use Tauri’s shell plugin to run ollama serve as a background process if the health check to http://localhost:11434/api/tags fails. Show a loading screen while Ollama starts and the model loads, then transition to the chat interface once the API responds. This removes the manual “start Ollama first” step from the user’s workflow and makes the app feel like a single integrated tool rather than two separate applications that need to be started in the right order. Managing Ollama’s lifecycle from within the Tauri app is one of the most impactful UX improvements you can make for non-technical users who do not want to think about backend services.
Persisting Conversation History
Unlike a browser-based app where localStorage or IndexedDB store conversation history, a Tauri app has access to the full file system through its permissions system. Use Tauri’s path plugin to get the app data directory — a platform-appropriate location like ~/.local/share/your-app/ on Linux, ~/Library/Application Support/your-app/ on macOS, and %APPDATA%\your-app\ on Windows — and write conversation history as a JSON file there. Load it on app startup and save after every message exchange. This gives users persistent conversation history that survives app restarts, without needing a database or a server, and that is stored in a predictable location they can back up or transfer to another machine if needed.
For a more structured approach to data persistence, use the tauri-plugin-store plugin which provides a simple key-value store backed by a JSON file in the app data directory. The store API is straightforward: store.set(key, value) and store.get(key), with automatic serialisation and a configurable auto-save interval. This is simpler than raw file I/O and handles concurrent writes correctly, making it the recommended approach for most Tauri apps that need persistent state without the complexity of SQLite or another embedded database.
Tauri represents the most capable foundation for a local AI desktop assistant in 2026. The combination of a lightweight compiled binary, native OS integration, Rust’s safety guarantees for the backend, and the full flexibility of the web platform for the UI makes it uniquely well-suited to building the kind of fast, reliable, always-available AI tool that genuinely improves a developer’s daily workflow. The initial learning curve of Tauri and Rust pays back quickly once the application is shipped — native performance, minimal resource usage, and OS integration that no web-based alternative can match.