Repository navigation
Architecture
SnipVault is a single frontend that runs in two environments — a Next.js web server and a Tauri desktop shell — sharing all UI code but swapping the storage backend.
The key abstraction is lib/tauri-api.ts. It exposes the same functions — getSnippets, createSnippet, updateSnippet, deleteSnippet — and picks a backend at runtime:
export function isTauri(): boolean {
return typeof window !== "undefined" && "__TAURI_INTERNALS__" in window;
}-
In Tauri, it calls Rust via
invoke("get_snippets", …). -
In the browser, it calls the REST API via
fetch("/api/snippets").
The React components never know which one they're talking to.
┌─────────────────────────────┐
│ React UI (components/) │
│ Snippet form, search, … │
└──────────────┬──────────────┘
│
lib/tauri-api.ts (isTauri?)
┌───────────┴───────────┐
browser│ │Tauri
▼ ▼
app/api/snippets/* src-tauri (#[tauri::command])
(Next.js routes) lib.rs → db.rs
│ │
better-sqlite3 rusqlite
▼ ▼
./data/snippets.db <local data dir>/snipvault/snippets.db
-
Routes:
app/api/snippets/*(GET list, POST create, PUT/PATCH/DELETE by id,/copy,/restore), plusapp/api/healthandapp/api/syncfor the sync feature. A rootproxy.ts(Next 16 proxy convention) gates all/api/*behind a bearer token whenSNIPVAULT_TOKENis set. -
Storage:
lib/db.tsopens abetter-sqlite3database at./data/snippets.db(lazily, on first query — sonext buildnever races to open it), enables WAL mode, and creates/migrates the schema.
-
Commands:
src-tauri/src/lib.rsregisters the snippet, database-management, and sync#[tauri::command]s (see API and Commands) and holds aMutex<AppState>with the DB handle. -
Storage:
src-tauri/src/db.rsusesrusqlite, storing the DB under the OS local data dir (dirs::data_local_dir()/snipvault/snippets.db), WAL mode, same schema.
Both runtimes are local-first: snippet reads and writes always hit the local database. On top of that, the desktop app can reconcile with a self-hosted server — which is just the SnipVault web app, containerized. A sync is one POST /api/sync round trip (via the Tauri HTTP plugin, so CSP/CORS don't apply): the client pushes its records, the server merges them by uuid with newest-updated_at-wins, and returns its merged set for the client to apply the same way. Deletes travel as tombstones. There's no live/remote data path — the server is only contacted during a sync. See Syncing and Data Storage.
Because Next.js static export (output: "export") can't coexist with app/api route handlers, the desktop build temporarily removes app/api during export. See Building and Packaging for the mechanics of scripts/build-tauri.mjs.
| Area | Technology |
|---|---|
| Framework | Next.js 16 (App Router, Turbopack), React 19 |
| Desktop shell | Tauri 2 (Rust) |
| UI components | shadcn/ui + Radix UI |
| Styling | Tailwind CSS 3 |
| Forms/validation | React Hook Form + Zod |
| Syntax highlighting | highlight.js |
| Theming | next-themes |
| Web DB | better-sqlite3 |
| Desktop DB | rusqlite (+ serde, chrono, dirs, uuid) |
| Sync transport |
@tauri-apps/plugin-http (desktop) → server /api/sync
|
| Secret storage |
keyring crate — OS credential store for the sync token (desktop, v2.6.0)
|
| External links |
@tauri-apps/plugin-opener — "Run in…" launcher opens URLs in the system browser (desktop, v2.15.0)
|
The web and desktop backends implement the same query logic (language/tag/search-mode filters, and the pinned-first sort orders — newest / most-used / recently-used / A–Z, from a shared allow-list) and the same schema, so data and behavior are portable between them. The Snippet shape is mirrored in TypeScript (lib/tauri-api.ts, lib/db.ts) and Rust (db.rs). See Data Storage and API and Commands.
Using SnipVault
Development
Operations