(04) Sistemi / Caso di studio
yourFinance
I saldi li garantisce il database.
yourFinance è un'app di finanza personale multi-utente: un'API in Rust, PostgreSQL e un solo client per iOS, Android, web e desktop. Il codice è privato; qui racconto com'è costruita e perché.
- Inizio
- Luglio 2025
- Versione
- 1.2.0, ottobre 2026
- Team
- Due sviluppatori
- Codice
- Privato
Rust · Axum · SQLx · PostgreSQL 18 · Expo · React Native · Tauri · Docker
Il problema
I dati di finanza personale servono solo se ci si può fidare dei saldi. L'app è usata da più persone su più dispositivi, e un errore in un client o in un handler non deve poter lasciare un conto con la cifra sbagliata.
Deve anche gestire più valute, importare tutto lo storico di un'altra app (Money Manager) e mostrare statistiche che non sono mai vecchie.
Vincoli
- L'API non scrive mai un saldo: ogni variazione è un movimento, anche la rettifica.
- Ogni riga personale appartiene a un utente, e PostgreSQL ripete la regola con la row-level security su ogni connessione del pool.
- Un solo codice per iOS, Android e web; l'app desktop è l'export web dentro una shell Tauri.
- Una modifica allo schema o all'API deve rompere la build, non l'app in esecuzione: l'SQL è verificato in compilazione e i tipi del client sono generati dal documento OpenAPI.
Architettura
Tre livelli: il client presenta soltanto, l'API autentica, valida e aggrega, il database è la fonte di verità. L'API è un workspace Cargo di crate a strati: ognuno dipende solo da quelli sotto, e la CI verifica che il dominio resti senza I/O.
yourf-api
HTTP: rotte sottili, autenticazione, mappatura degli errori, OpenAPI.
yourf-worker
Job in background: notifiche push, riepilogo degli avvisi, movimenti ricorrenti, riconciliazione dei saldi.
yourf-runtime
Ciò che i due binari condividono: configurazione, telemetria, ciclo di vita.
yourf-db
Repository SQLx, ambito della row-level security, scritture in una sola transazione.
yourf-import
Lettori dei file di altre app verso un formato neutro, senza database.
yourf-domain
Modelli, contratti e regole di business. Nessun I/O.
Due binari, una sola immagine: API e worker si rilasciano e scalano separatamente.
Una richiesta, dall'inizio alla fine
La creazione di un movimento:
- 01Client
Il form valida la bozza e la invia con il token.
- 02API
Autentica il token, carica l'utente, valida il corpo e inserisce il movimento con le sue ripartizioni per conto e categoria in una sola transazione.
- 03Database
I trigger differiti controllano che le ripartizioni tornino e che conti, categorie e tag siano dell'utente, poi aggiornano i saldi. Una violazione annulla tutto e diventa un 400 con un codice di regola stabile.
- 04Client
Se va a buon fine, elenchi, totali e dashboard si ricaricano.
Scelte e compromessi
- 01
L'integrità sta nel database
Saldi, proprietà dei dati e regole sulle categorie sono garantiti dai trigger. Significa scrivere logica in SQL invece che in Rust; in cambio nessun client e nessun handler può corrompere i dati.
- 02
Saldi verificabili, non una partita doppia completa
Ogni effetto su un saldo è una registrazione con segno, con un'unica definizione usata sia dal trigger che scrive i saldi sia dal job che li riconcilia. Una partita doppia completa varrebbe la riscrittura solo se una funzione avesse bisogno di uno stato patrimoniale.
- 03
Un trasferimento è un movimento con due importi
Quanto è uscito dal conto di origine e quanto è arrivato a destinazione. La differenza copre sia un cambio di valuta sia una commissione, che quindi non compare come spesa nelle statistiche.
- 04
Nessuna coda di job
Il worker prende le righe in scadenza con FOR UPDATE SKIP LOCKED e la coda delle push è svegliata da LISTEN/NOTIFY. Ogni passata è idempotente: eseguirla due volte, o su più repliche, dà lo stesso risultato.
- 05
Statistiche calcolate a ogni richiesta
Dashboard e statistiche aggregano i movimenti in SQL ogni volta: nessun aggregato precalcolato e nessuna cache, quindi le cifre non sono mai vecchie. Il prezzo è che le query devono essere veloci da sole.
Misure
Il profiling delle query analitiche ne ha mostrate due che crescevano con tutto lo storico invece che con il periodo scelto. Con il piano generico di un prepared statement, il planner sceglieva un nested loop che rieseguiva un join per ogni movimento. Gli indici non c'entravano: la correzione è stata riscrivere la vista in modo che ogni ramo parta dal movimento.
| Query | Prima | Dopo |
|---|---|---|
| Flussi giornalieri (andamento delle statistiche)daily_flows | 64.5 ms | 1.9 ms |
| Spese essenziali (riepilogo della dashboard)essential_expenses | 48.0 ms | 1.5 ms |
L'app



Cosa viene dopo
- Una scadenza sui conti, con un avviso prima che arrivi.
- L'ammortamento: un costo ripartito su più mesi nelle statistiche, mentre il saldo resta di cassa.
- Un assistente che risponde a domande su statistiche e conti con strumenti di sola lettura: il modello sceglie lo strumento, ogni numero lo calcola il backend.
Codice e demo
Il repository è privato. Su richiesta mostro il codice e l'app in funzione.