SQLite in a Tauri v2 App — Simple, Reliable, Zero Regrets

All tests run on an 8-year-old MacBook Air. All results from shipping 7 Mac apps as a solo developer. No sponsored opinion.

I added SQLite to three Tauri apps. Every time I reached for it later than I should have. Here’s how I actually use it and why I stopped overthinking the database question.

Why SQLite and not something else

A Tauri app runs on the user’s machine. There’s no server. There’s no network. The database lives in the app’s data directory. SQLite is a file. It’s fast, reliable, and has zero operational overhead.

For a solo dev shipping desktop apps, this is the correct choice. There’s no meaningful alternative worth evaluating.

I use rusqlite directly. No ORM. The queries are simple enough that an ORM adds complexity without benefit.

What I actually use it for

Sync state tracking. HiyokoAutoSync tracks which files have been synced, when, and their hash. Without SQLite this is a mess of JSON files with race condition potential. With SQLite it’s a straightforward table with indexed lookups.

CREATE TABLE sync_records (
    id INTEGER PRIMARY KEY,
    file_path TEXT NOT NULL,
    last_synced INTEGER NOT NULL,
    file_hash TEXT NOT NULL,
    direction TEXT NOT NULL
);

App settings that need querying. Simple key-value settings belong in a config file. Settings you need to query — filter, sort, paginate — belong in SQLite.

History and logs. Anything the user might want to browse, search, or filter later. SQLite makes this trivial. A flat file makes this painful.

The Tauri-specific setup

Database path via app_handle:

let db_path = app_handle
    .path()
    .app_data_dir()
    .unwrap()
    .join("app.db");

let conn = Connection::open(&db_path)?;

I initialize the schema on first launch with CREATE TABLE IF NOT EXISTS. Simple, idempotent, no migration framework needed at this scale.

For async Tauri commands, I wrap the connection in Mutex and manage it as app state:

app.manage(Mutex::new(conn));

Not glamorous. Works perfectly.

Where it gets annoying

Blocking calls in async context. rusqlite is synchronous. Tauri commands are async. You need to either use spawn_blocking or accept that you’re blocking the thread. For most desktop app use cases, blocking is fine — queries complete in microseconds.

Schema changes. When I need to add a column, I write a manual migration check on startup. For 3-4 migrations across an app’s lifetime, this is manageable. If you’re doing frequent schema changes, reach for a migration library earlier than I did.

The verdict

SQLite via rusqlite is the correct default for Tauri app persistence. Add it earlier than you think you need it. The migration from “JSON files everywhere” to SQLite mid-project is annoying and avoidable.

The database question for Tauri desktop apps is solved. SQLite. Move on.

TL;DR: SQLite via rusqlite is the right call for Tauri desktop apps — no server, no deps, zero ops overhead. Use app_data_dir() for the DB path, Mutex as app state, and CREATE TABLE IF NOT EXISTS for schema init. Add it earlier than you think you need it; migrating from JSON files mid-project is painful.

If this was useful, a ❤️ helps more than you’d think — thanks!

HiyokoAutoSync | X → @hiyoyok

Total
0
Shares
Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Post

How to Secure Storage Using Azure Blob Storage and Azure Files

Related Posts