Tauri, React, and Rust — the stack
Every app is a bet on a stack. Glifo bets on Tauri for the shell, React for the UI, and Rust for everything that touches the filesystem, git, or the network. Here's why.
Why Tauri
Tauri gives you a native window with a system webview (WKWebView on macOS) and a Rust backend. Compared to Electron:
| Electron | Tauri | |
|---|---|---|
| App size | ~150 MB | ~8 MB |
| Memory | ~200 MB idle | ~40 MB idle |
| Backend | Node.js | Rust |
| Webview | Bundled Chromium | System |
The system webview means native performance, macOS integration (NSVisualEffectView, Keychain, system theme), and a tiny bundle. The trade-off is less control over the rendering engine — but WKWebView on modern macOS is excellent.
Why React
The UI is a single-page React app with CodeMirror 6 for the editor. React gives us:
- Component composition for complex UI (sidebar, editor, preview, settings, publish wizard)
- Fine-grained re-rendering with hooks and memoization
- A massive ecosystem for things like markdown rendering (
markdown-it) and syntax highlighting (highlight.js)
The alternative was a native SwiftUI or AppKit UI. But the web platform is better for rich text editing — CodeMirror, ProseMirror, and friends are decades ahead of native text frameworks for this use case.
Why Rust
Rust handles everything outside the browser:
- File I/O — reading/writing notes, watching for changes
- Git operations — commits, diffs, history via
git2 - Publishing — markdown parsing, HTML generation, GitHub API calls
- Config management — TOML parsing, file watching, state management
- Platform integration — NSVisualEffectView, Keychain, window management via
objccrate
Rust's ownership model means no GC pauses during file operations. serde makes JSON/TOML serialization trivial. And Tauri's command system provides type-safe IPC between the frontend and backend.
The IPC boundary
React calls Rust through Tauri commands — async functions decorated with #[tauri::command]:
#[tauri::command]
pub async fn publish_to_github(
notes: Vec<NoteToPublish>,
repo: String,
site: SiteConfig,
) -> Result<String, String> {
// Rust handles all the work
}
On the frontend, it's a simple invoke:
const url = await invoke<string>('publish_to_github', { notes, repo, site });
Serialization happens automatically via serde. Types are checked at compile time on the Rust side. The frontend gets type safety through TypeScript interfaces that mirror the Rust structs.
What we'd change
State management could be simpler. We use a mix of React context, local state, and Tauri events. A more unified approach (Zustand or Jotai) would reduce prop drilling.
CSS architecture grew organically. We split globals.css into focused files, but a proper design token system would help as the UI grows.
Testing is the weakest link. Rust code has some tests, but the React layer is mostly untested. Integration tests for the IPC boundary would catch the most bugs.
The stack isn't perfect, but it's honest: use each tool for what it's best at. The browser for UI, Rust for systems work, and Tauri to glue them together.