Skip to content

Data Flow

Data flow

OwnerVidar (Master of Craft)
Last reviewed2026-05-07 by Vidar
Next review2026-08-07
Source pathssrc/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"| Client

Components

SourceResponsibility
src/tauri/*Client.tsPer-domain invoke() wrappers. One file per domain (transactions, rules, tags, groups, invites, …). Thin: no business logic, just error surfacing.
src/store/*.tsZustand 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.rsAppState 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-render

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.

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-fetch

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

FailureWhat happensRecovery
SQLite error (constraint, I/O)sqlx::Error → wrapped in AppError → serialized to frontend as JS ErrorCheck 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 messageRun cargo test in src-tauri/ to regenerate bindings; ensure both sides compiled from the same types
Changeset capture failureCommand logs a WARN and returns Ok(()) — the write still succeeds; changeset is skippedManual sync reconciliation once CAS-1093 lands
Concurrent write (two commands writing same row)SQLite serialises via its internal WAL; no corruption, but last-write winsZustand 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 initN/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). ChangesetService becomes the sync publisher; the write path gains an async flush step. The cloud_snapshot_service.rs scaffolding is already in place.
  • CAS-1100 — Licensing activation gate: AppState already carries LicenseService; the gate call before premium commands is the remaining wiring.
  • AI featuresai_commands.rs and services/ai/ (Anthropic + Gemini) are scaffolded but not yet exposed in the UI. The data path will be: frontend → ai_commands.rsAiManager → 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.

See: CHANGELOG → 2026-05-18