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.
flowchart LR subgraph Frontend["Frontend (WebView)"] UI[React component] Store[Zustand store] Client["tauri/ client<br/>(invoke wrapper)"] UI <--> Store Store --> Client end
IPC["Tauri IPC<br/>(JSON over postMessage)"]
subgraph Backend["Backend (Rust process)"] Cmd["Command handler<br/>src-tauri/src/commands/"] State["AppState<br/>(Arc-wrapped services)"] Svc["Service layer<br/>src-tauri/src/services/"] Repo["Repository layer<br/>src-tauri/src/repository/"] DB[(SQLite)] Cmd --> State State --> Svc Svc --> Repo Repo --> DB end
Client -->|"invoke('command_name', args)"| IPC IPC --> Cmd Cmd -->|"Result<T, AppError>"| IPC IPC -->|"T or throws"| ClientComponents
| 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
sequenceDiagram participant C as React component participant S as Zustand store participant Cl as transactionClient.ts participant IPC as Tauri IPC participant Cmd as transaction_commands.rs participant Svc as TransactionService participant Repo as transactions_db.rs participant DB as SQLite
C->>S: reads store state (selector) S->>Cl: getTransactions() Cl->>IPC: invoke('get_transactions') IPC->>Cmd: get_transactions(state) Cmd->>Svc: service.get_all_transactions() Svc->>Repo: sqlite_db.get_transactions(...) Repo->>DB: SELECT ... FROM transactions DB-->>Repo: rows Repo-->>Svc: Vec<TransactionRow> Svc-->>Cmd: Ok(Vec<TransactionRow>) Cmd-->>IPC: Result::Ok(rows) → JSON IPC-->>Cl: TransactionRow[] Cl-->>S: store updated S-->>C: re-renderWrite — 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.
sequenceDiagram participant C as React component participant Cl as transactionClient.ts participant IPC as Tauri IPC participant Cmd as transaction_commands.rs participant Svc as TransactionService participant CS as ChangesetService participant Repo as transactions_db.rs participant DB as SQLite
C->>Cl: updateTransactionRow(row) Cl->>IPC: invoke('update_transaction_row', {request}) IPC->>Cmd: update_transaction_row(state, request) Cmd->>Repo: get_transactions_by_ids([id]) — snapshot before Repo-->>Cmd: before_val Cmd->>Svc: transaction_service.update_transaction(row) Svc->>Repo: UPDATE transactions SET ... Repo-->>Svc: Ok(()) Svc-->>Cmd: Ok(()) Cmd->>CS: capture_and_save(before, after, vault_id, device_id) CS->>DB: INSERT INTO changesets ... Cmd-->>IPC: Result::Ok(()) IPC-->>Cl: void C->>C: optimistic or re-fetchType 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.