Architecture & Decisions
Finia is a Leptos + Tauri 2 + SQLite desktop application with an optional, end-to-end-encrypted sync layer.
Data flow: frontend to backend
Leptos views read from reactive stores (crates/app/src/stores/), which call
typed invoke wrappers in crates/tauri-ipc/. The command handlers
(src-tauri/src/commands/) acquire a tokio::sync::Mutex lock on the single
shared rusqlite::Connection held in AppState, execute SQL, and return
serialized structs.
|
All monetary values are stored and passed as integer cents ( |
A convention worth knowing: mutation handlers split into a synchronous
_impl(&mut Connection, …) → Outcome core plus a thin async wrapper that emits
Tauri events. This keeps the SQL testable and the event side-effects in one place.
CRDT sync
The sync engine lives in src-tauri/src/sync/crdt.rs:
-
Conflict resolution is Last-Write-Wins per field using
LwwRegister, keyed by a Hybrid Logical Clock (HLC):(wall_ms, counter, node_id). -
Events are append-only; the merge function is commutative, associative, and idempotent.
Outbound events are generated in the command handlers. Every mutation to a
synced entity calls sync::emit::emit / emit_tombstone inside the same
rusqlite transaction as the row write — a transactional outbox into
sync_events. The device HLC lives in AppState.hlc. On the receive side,
sync::client::apply_entity_to_db upserts (ensure-row, then per-field UPDATE) so
peer-created rows materialize, and applies a __deleted tombstone as a hard delete.
What replicates
src-tauri/src/sync/registry.rs is the single source of truth for what
replicates — entity to table to fields to required columns. It is consumed by
emit, apply, parse_entity_type, and backfill.
Phase-1 scope is the six core entities: account, transaction, budget, recurring_transaction, contact, and category. Loans and schedules, assets and mortgages, portfolios/trades/lots, and reconciliation linking are deliberately not synced yet. Receiving an event for an unregistered entity is safe — it is logged, not materialized — so promoting an entity later is additive.
Sync roles
The app can act in two roles simultaneously:
Hub / server |
An embedded Axum server spawned inside the Tauri process ( |
Spoke / client |
A background push/pull loop ( |
Both are started and stopped via Tauri commands (commands/sync.rs) and persist
their settings in SQLite.
Encryption
Secret columns are encrypted at rest with ChaCha20-Poly1305; the master key
lives in the OS keyring with a 0600 file fallback, swept idempotently at
startup. Sync payloads are additionally encrypted client-side (zero-knowledge):
a relay only ever sees ciphertext. The encryption key is wrapped by the user’s
master passphrase.
i18n
The default locale is de. Translations live under crates/app/src/i18n/
(de.json, en.json, ru.json) and are loaded at compile time via
load_locales!(). The active locale is persisted in the PrefsStore (SQLite).
|
Reserved Rust keywords cannot be i18n keys — |
Workspace layout
|
UI primitives + chart components; no IPC dependency; builds for wasm32. |
|
Typed |
|
The Leptos frontend; wasm32 only. |
|
Pure-Rust CSV parsers for ING and Postbank. |
|
Tauri shell + SQLite handlers; does not build for wasm32 (pulls |
|
Standalone Axum relay; a separate |
The embedded hub server inside src-tauri/ is a lighter variant wired directly to
the app’s SQLite; it is not the same binary as the standalone sync-server/.