Data Flow
Data flow
| Owner | Vidar (Master of Craft) |
| Last reviewed | 2026-05-07 by Vidar |
| Next review | 2026-08-07 |
| Source paths | src/tauri/, src-tauri/src/commands/, src-tauri/src/services/, src-tauri/src/repository/, src-tauri/src/types/app_state.rs |
What it is
The hot path every user action travels: from a React component calling
invoke() on the frontend, through Tauri IPC, into a command handler,
down through a service, and into the SQLite repository — then the same
path in reverse carrying the result.
How it fits
This is the interior of the DesktopApp box in the system-level diagram
(README.md). Everything here is local: no network
involved in normal read/write operations. Network paths (sync, licensing,
email) are documented in their own files.
Components
| Source | Responsibility |
|---|---|
src/tauri/*Client.ts | Per-domain invoke() wrappers. One file per domain (transactions, rules, tags, groups, invites, …). Thin: no business logic, just error surfacing. |
src/store/*.ts | Zustand stores. Each domain owns one store. Components read from stores; stores call the client files to load/mutate. |
src-tauri/src/commands/ | #[tauri::command] functions. 25+ command modules: transactions, rules, groups, invites, tags, periods, invoices, receipts, sync, encryption, AI, telemetry, backup, and more. Registered in main.rs via generate_handler![]. |
src-tauri/src/types/app_state.rs | AppState struct injected into every command via State<'_, AppState>. Holds Arc-wrapped service instances. |
src-tauri/src/services/ | Business logic, 20+ services. Notable additions since initial docs: ai/ (Anthropic + Gemini providers), sync/ (relay transport via WebSocket), group_service.rs, invite_service.rs, receipt_service.rs, telemetry_service.rs, profile_service.rs, license_service.rs, cloud_snapshot_service.rs. |
src-tauri/src/repository/ | SQLite access via SQLx. One _db.rs file per domain — transactions, rules, tags, groups, invites, periods, profiles, receipts, changesets, devices, providers, telemetry, and more. All queries are async, execute on a Tokio runtime. |
src-tauri/src/types/ | Shared Rust types annotated with #[derive(Type, Serialize, Deserialize)] (ts-rs). Running cargo test regenerates src/types/bindings/. |
Data flow
Two canonical paths: read and write.
Read — get_transactions
Write — update_transaction_row
Writes follow the same path, with two extra steps: a before-snapshot is taken for changeset capture, and the changeset is saved after the write so the sync system has a record of the mutation.
Type bindings
Rust types in src-tauri/src/types/ carry #[derive(TS)] from ts-rs.
Running cargo test in src-tauri/ regenerates every file under
src/types/bindings/. Never edit bindings by hand. If the frontend
and backend disagree on a field, the IPC call fails at runtime with a
JSON deserialization error — run cargo test and commit the updated
bindings.
AppError propagation
Commands return Result<T, AppError>. AppError implements
serde::Serialize so Tauri serializes it as JSON and delivers it as a
JS Error on the frontend. The client wrappers in src/tauri/ surface
these as thrown exceptions (or use showNotification for user-visible
write failures).
Failure modes + recovery
| Failure | What happens | Recovery |
|---|---|---|
| SQLite error (constraint, I/O) | sqlx::Error → wrapped in AppError → serialized to frontend as JS Error | Check app logs via get_recent_logs; restart is safe (no in-flight transactions) |
| IPC deserialization (type mismatch) | Tauri rejects the call before the command runs; frontend receives a JS Error with the serde error message | Run cargo test in src-tauri/ to regenerate bindings; ensure both sides compiled from the same types |
| Changeset capture failure | Command logs a WARN and returns Ok(()) — the write still succeeds; changeset is skipped | Manual sync reconciliation once CAS-1093 lands |
| Concurrent write (two commands writing same row) | SQLite serialises via its internal WAL; no corruption, but last-write wins | Zustand optimistic updates can diverge; a full re-fetch resolves |
| Service unavailable (service not initialized) | Rust compile-time: services are constructed in AppState::new() at startup; no late init | N/A — startup failure exits the process |
What’s planned to change
- CAS-1093 — Cross-device sync: the changeset table (populated on
every write today) will be flushed to a Cloudflare Worker via the
sync/service (WebSocket transport).ChangesetServicebecomes the sync publisher; the write path gains an async flush step. Thecloud_snapshot_service.rsscaffolding is already in place. - CAS-1100 — Licensing activation gate:
AppStatealready carriesLicenseService; the gate call before premium commands is the remaining wiring. - AI features —
ai_commands.rsandservices/ai/(Anthropic + Gemini) are scaffolded but not yet exposed in the UI. The data path will be: frontend →ai_commands.rs→AiManager→ provider (HTTP call) → result back via IPC.
Last reviewed: 2026-05-07 by Vidar. Next review: 2026-08-07.
What changed {#what-changed}
This chapter was introduced in CAS-3637 Phase 3 (The Casaconomy Book) as the canonical reference for the Tauri IPC data-flow path.