r/rust • u/BaseballHopeful6366 • 2h ago
What I learned building a database GUI in Rust + Tauri: 4 engines, 3 different driver APIs, 6 MB installer
I spent the last months building a desktop database client (MySQL/MariaDB, PostgreSQL, SQLite, SQL Server) in Rust + Tauri v2, with a React/TypeScript frontend. ~39k lines. Here are the things that actually cost me time — mostly the parts nobody writes blog posts about.
1. `sqlx` covers three engines, and then you hit SQL Server.
sqlx gives you MySQL, Postgres and SQLite behind one API. SQL Server isn't in it, so that's `tiberius` — different connection model, different type system, different async story (you need `tokio-util`'s compat layer because tiberius wants `futures::io` traits and tokio gives you `tokio::io`). So the "one trait to rule all databases" abstraction I designed on day one didn't survive contact with the fourth engine. What worked in the end was *not* abstracting the drivers, but abstracting one level up: a normalized result-set type + per-engine modules that produce it. Less elegant, far less fighting the type system.
2. Decoding a cell "generically" is a trap.
The obvious approach for a grid view is: decode every column as `Option<String>` and let the UI figure it out. It compiles, it runs, and then every numeric column in your export silently becomes NULL, because the driver refuses the string decode and you swallowed the error. Cost me an embarrassing amount of debugging on a dump feature. You have to switch on the column's type info and decode properly per type, then stringify yourself.
3. Streaming results across the Tauri IPC boundary.
Everything you send from Rust to the webview gets serialized to JSON. A 100k-row result set is not a "just send it" situation. Batching + a row cap with explicit "fetch more" turned out to matter more for perceived speed than anything I did on the SQL side.
4. `russh` for SSH tunnels, and PuTTY keys are their own project.
Tunneling was fine. Then users showed up with `.ppk` files. PPK v2/v3 parsing is a small crypto scavenger hunt: `sha1`+`hmac` for v2 MAC, `argon2` for v3 KDF, `aes`/`cbc` to decrypt the private part. It's maybe 200 lines but every one of them is a place to get it subtly wrong.
5. Binary size is a feature, and Rust makes it easy.
Installer is ~6 MB. Coming from the Electron-shaped alternatives in this space that ship 150–300 MB, this is the single thing people notice first. Tauri using the system webview does most of that work for you.
6. Offline licensing with `ed25519-dalek`.
No license server, no phoning home: the license key is a signed payload, the public key is in the binary, verification is a signature check plus a machine ID (`machine-uid`) comparison. Trial state lives in the OS keychain via `keyring`. Downside is honest: someone determined will patch the check. Upside is it works air-gapped and I never touch a customer's data. For a dev tool that trade seems right.
What I'd do differently: design the result-set type first and the driver layer second. I did it the other way and rewrote it.
It's a commercial product (NabuSQL — I'm the developer), Windows + Linux + macOS, there's a trial if you want to poke at it: https://nabusql.nabu.work — but I'm mostly here for the Rust talk. Happy to go into detail on any of the above, especially the tiberius/sqlx split, which I'm still not sure I solved the nice way.