Architecture

The two-world desktop model — a privileged Rust core and a sandboxed webview joined by typed, capability-scoped IPC.

The mental model is two worlds joined by one narrow, typed boundary.

Two worlds

WorldWhat it isWhat it does
Rust core (privileged)src-tauri/ — the native binary, plugins, and your #[tauri::command]sTouches the OS: keychain, filesystem, notifications, updater, tray, logging.
Webview (sandboxed)src/ — a React SPA in the system webview, HashRouter, lazy routesRenders UI and calls the core. No Node, no preload script, no direct OS access.

The boundary between them is invoke plus the capabilities ACL. UI code never reaches the OS directly — it asks the core, and the core only answers for permissions a capability has granted. This is the security difference from a web template and from the Electron sibling: instead of a preload bridge, each window carries an enumerated permission set and the Rust core rejects anything outside it before your code runs.

IPC in one screen

component  →  tauri.<domain>.<method>()  →  generated bindings  →  #[tauri::command] (Rust)
             (or useTauri('<domain>'))      src/lib/bindings.ts     src-tauri/src/commands/
  • Types are compiler-derived by tauri-specta — there is no hand-written schema to keep in sync. The generated src/lib/bindings.ts is committed and CI fails on drift.
  • The ACL checks permissions before the command body runs.
  • Zod is reserved for settings and forms, not IPC args.

The full add-a-command walkthrough lives on the IPC page.

A use-conveyor.ts shim and src/lib/conveyor/schemas.ts survive from the Electron port and are still imported by some components. Treat tauri / useTauri as the real API and the conveyor layer as port-compat — some of its methods are no-op stubs. See IPC.

Central configuration

All frontend configuration is read once in shared/config.ts; nothing else touches import.meta.env. It exposes features, billingConfig, showcaseConfig, and the analytics/Sentry slots.

Flag (features)EnvDefault
authVITE_AUTH_ENABLEDon
autoUpdateVITE_AUTO_UPDATE_ENABLEDon
showcaseVITE_SHOWCASE_ENABLEDon
billingVITE_BILLING_ENABLEDoff

A flag strips its whole subsystem at build time. Crucially, there is one flag per subsystem — no MAIN_VITE_* twin like Electron. The native menu and tray are built in TypeScript (src/lib/menu/, src/lib/tray/) and read the same features object directly, so a single VITE_SHOWCASE_ENABLED reaches the routes, sidebar, palette, and the native Template menu.

Some template comments and the README describe the frontend "pushing" flags to Rust at boot via an app::set_feature_flags command. That command does not exist and getNativeFeatureFlags() has no callers. The single-flag behavior is real, but the reason is simpler: the menu/tray are JS-built and read features directly. There is no config push.

State layers

LayerHoldsBacked by
zustand storesSession and entitlement state (auth-store, subscription-store)supabase-js
TanStack QueryServer data (todos, subscription reads)src/lib/query/
Settings storeUser preferencestauri-plugin-store + versioned TS-side migrations

Startup flow

The Rust entry (main.rslib.rs run()) wires the app in a deliberate order:

  1. Single-instance plugin first — so a second launch is short-circuited before any window exists; its callback focuses the running window and forwards the new process's argv to the frontend intent dispatcher.
  2. Plugins — opener, store, dialog, fs, autostart, log, notification, global-shortcut, os, process, updater, window-state, and deep-link.
  3. Setup hook — in debug builds only, registers the soar-tauri:// scheme at runtime (deep_link().register_all()) so links are testable during tauri dev; initializes the optional Sentry guard.
  4. Window — created from tauri.conf.json with decorations: false (the frameless custom titlebar).
  5. Frontendmain.tsx validates the Supabase env, then mounts AppOnboardingGate → routes.

With features.auth on and the Supabase env missing or invalid, main.tsx renders SupabaseConfigErrorScreen and never imports App (whose store imports would otherwise construct the Supabase client). With auth off, the app mounts and the client is never touched.

The second defense layer: CSP

Beyond the ACL, tauri.conf.json app.security sets a strict Content Security Policy. The default is anchored to 'self'; the notable relaxation is connect-src, which allows the IPC transport plus outbound HTTPS/WSS for Supabase and the update feed:

"connect-src 'self' ipc: http://ipc.localhost https: wss:"

ipc: and http://ipc.localhost are how the webview reaches the Rust core; script-src 'self' (dev adds 'unsafe-inline'/'unsafe-eval' for HMR) means nothing in-app can inject or load foreign scripts. Together the CSP and the ACL keep the webview pinned to self — external URLs are forced out through the audited opener chokepoint (see Capabilities).

On this page