Tauri
Electron · Tauri · Qt · Flutter · React Native · Capacitor and Ionic · .NET MAUI
A Tauri application keeps its database on the Rust side and exposes commands to the web view. The Rust crate provides the whole integration. The Rust installation page applies.
Requirements
Section titled “Requirements”Tauri 2 with a stable Rust toolchain, on macOS, glibc Linux or Windows. Tauri’s mobile targets are not part of the release.
What ships
Section titled “What ships”The Rust column of the compatibility matrix, packaged for Tauri. Each release runs the quickstart on the platforms in this table. Other platforms have no verification for this integration.
| Platform | Targets | Package |
|---|---|---|
| macOS | macos-arm64, macos-x86_64 | potluckdb |
| Linux | linux-x86_64-gnu, linux-arm64-gnu | potluckdb |
| Windows | windows-x86_64, windows-arm64 | potluckdb |
Install
Section titled “Install”cargo add potluckdbBuilding downloads the target’s engine and embeds its extracted paths, as the Rust installation page describes. A bundled application ships the library where the crate searches: beside the executable or in a macOS bundle’s Contents/Frameworks. The application that the release builds places it there through Tauri’s bundle settings.
Quickstart
Section titled “Quickstart”The setup hook opens the database in the application’s data directory. It registers the database and remote together as managed state. Two commands add and list notes. Shutdown closes the database before the remote. See Concepts for the remote model.
Tauri does not accept a public command in lib.rs itself, so the commands are in a module.
The page shows stored notes and reports write failures.
- Save the Rust file as
src-tauri/src/notes.rs. - Replace the contents of
src-tauri/src/lib.rswith the two lines below. - Save both page files in the directory that
build.frontendDistselects. - Set
app.withGlobalTauritotrueso the page can invoke the commands. - Have the desktop entrypoint call the library’s
run()and pass its returned exit code tostd::process::exit. - On an error, make the desktop entrypoint exit with a nonzero code.
- Run
cargo tauri dev. - Enter a note and select Save note.
mod notes;
pub use notes::run;//! A directory-backed note application: Rust owns the database, and the WebView//! reaches it through two registered commands.
use std::path::{Path, PathBuf};use std::sync::atomic::{AtomicBool, Ordering};use std::sync::{Arc, Mutex};
use potluckdb::{Database, Remote};use tauri::{Manager, State};
struct Owners { db: Option<Database>, remote: Option<Remote>, close_error: Option<String>,}
impl Owners { fn close(&mut self) -> Result<(), String> { let mut errors = Vec::new(); if let Some(mut db) = self.db.take() { if let Err(error) = db.close() { errors.push(error.to_string()); } // Release the database's shared exchange before closing the remote. drop(db); } if let Some(mut remote) = self.remote.take() { if let Err(error) = remote.close() { errors.push(error.to_string()); } } if !errors.is_empty() { self.close_error = Some(errors.join("; ")); } self.close_error.clone().map_or(Ok(()), Err) }}
pub struct Notes(Mutex<Owners>);
impl Notes { pub fn open(data: &Path, create: bool) -> Result<Self, String> { if create { std::fs::create_dir_all(data).map_err(|error| error.to_string())?; } else if !data.join("notes.potluckdb").exists() { return Err("the saved database is absent".into()); } let remote = Remote::from_directory(data.join("remote")) .map_err(|error| error.to_string())?; let mut owners = Owners { db: None, remote: Some(remote), close_error: None }; let opened = potluckdb::open_with_remote( data.join("notes.potluckdb"), owners.remote.as_ref().unwrap(), ); match opened { Ok(db) => owners.db = Some(db), Err(error) => { let cleanup = owners.close(); return Err(format!("{error}; cleanup: {cleanup:?}")); } } if create { if let Err(error) = owners.db.as_mut().unwrap().execute( "CREATE TABLE IF NOT EXISTS note(id INTEGER PRIMARY KEY, body TEXT NOT NULL)", (), ) { let cleanup = owners.close(); return Err(format!("{error}; cleanup: {cleanup:?}")); } } Ok(Self(Mutex::new(owners))) }
pub fn rows(&self) -> Result<Vec<(i64, String)>, String> { let mut owners = self.0.lock().map_err(|_| "the database lock is poisoned")?; let db = owners.db.as_mut().ok_or("the database is closed")?; let rows = db.query("SELECT id, body FROM note ORDER BY id", ()) .map_err(|error| error.to_string())?; rows.iter() .map(|row| Ok((row.get::<i64>("id")?, row.get::<String>("body")?))) .collect::<Result<_, potluckdb::PotluckDBError>>() .map_err(|error| error.to_string()) }
pub fn close(&self) -> Result<(), String> { let (mut owners, poisoned) = match self.0.lock() { Ok(owners) => (owners, false), Err(error) => (error.into_inner(), true), }; let closed = owners.close(); if poisoned { Err(format!("the database lock is poisoned; cleanup: {closed:?}")) } else { closed } }}
impl Drop for Notes { fn drop(&mut self) { if let Err(error) = self.close() { eprintln!("note application cleanup failed: {error}"); } }}
#[tauri::command]pub fn add_note(notes: State<Notes>, body: String) -> Result<(), String> { let mut owners = notes.inner().0.lock().map_err(|_| "the database lock is poisoned")?; let db = owners.db.as_mut().ok_or("the database is closed")?; db.execute("INSERT INTO note(body) VALUES(?)", (body,)) .map(|_| ()) .map_err(|error| error.to_string())}
#[tauri::command]pub fn list_notes(notes: State<Notes>) -> Result<Vec<(i64, String)>, String> { notes.rows()}
pub fn builder(data: Option<PathBuf>) -> tauri::Builder<tauri::Wry> { tauri::Builder::default() .setup(move |app| { let data = match data { Some(data) => data, None => app.path().app_data_dir()?, }; app.manage(Notes::open(&data, true).map_err(std::io::Error::other)?); Ok(()) }) .invoke_handler(tauri::generate_handler![add_note, list_notes])}
pub fn run_application(builder: tauri::Builder<tauri::Wry>) -> Result<i32, String> { let app = builder.build(tauri::generate_context!()).map_err(|error| error.to_string())?; let failed = Arc::new(AtomicBool::new(false)); let cleanup_failed = Arc::clone(&failed); let code = app.run_return(move |handle, event| { if matches!(event, tauri::RunEvent::ExitRequested { .. } | tauri::RunEvent::Exit) { if let Some(notes) = handle.try_state::<Notes>() { if let Err(error) = notes.close() { eprintln!("note application cleanup failed: {error}"); cleanup_failed.store(true, Ordering::SeqCst); } } } }); Ok(if failed.load(Ordering::SeqCst) { 1 } else { code })}
pub fn run() -> Result<i32, String> { run_application(builder(None))}<!doctype html><html lang="en"><head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Notes · potluckdb</title> <script src="renderer.js" defer></script></head><body> <main> <h1>Notes</h1> <form id="note-form"> <label for="note-body">New note</label> <input id="note-body" name="body" required autocomplete="off"> <button id="save-note" type="submit" disabled>Save note</button> </form> <p id="status" role="status">Opening notes…</p> <ol id="notes" aria-label="Saved notes"></ol> </main></body></html>"use strict";
const { invoke } = window.__TAURI__.core;const form = document.querySelector("#note-form");const body = document.querySelector("#note-body");const save = document.querySelector("#save-note");const list = document.querySelector("#notes");const status = document.querySelector("#status");
async function refresh() { const rows = await invoke("list_notes"); while (list.firstChild) list.removeChild(list.firstChild); for (const [id, text] of rows) { const item = document.createElement("li"); item.dataset.id = String(id); item.textContent = text; list.appendChild(item); }}
form.addEventListener("submit", async (event) => { event.preventDefault(); if (save.disabled || body.value.length === 0) return; save.disabled = true; try { await invoke("add_note", { body: body.value }); await refresh(); body.value = ""; status.textContent = "Note saved."; } catch (error) { status.textContent = `Could not save the note: ${error}`; } finally { save.disabled = false; }});
refresh().then(() => { save.disabled = false; status.textContent = "Ready.";}).catch((error) => { status.textContent = `Could not open notes: ${error}`;});Verified by the release
Section titled “Verified by the release”Each release builds this application on macOS, Linux and Windows and verifies both commands through the web view.