Skip to content

(04) Systems / Case study

yourFinance

The database guarantees the balances.

yourFinance is a multi-user personal finance app: a Rust API, PostgreSQL and one client for iOS, Android, web and desktop. The code is private; this page shows how it is built and why.

Started
July 2025
Version
1.2.0, October 2026
Team
Two developers
Code
Private

Rust · Axum · SQLx · PostgreSQL 18 · Expo · React Native · Tauri · Docker

The problem

Personal finance data is only useful if the balances can be trusted. Several people use the app from several devices, and a bug in a client or in a handler must not be able to leave an account with the wrong figure.

It also has to handle more than one currency, take in the full history of another app (Money Manager) and show statistics that are never stale.

Constraints

  • The API never writes a balance: every change is a movement, a balance adjustment included.
  • Every personal row belongs to one user, and PostgreSQL repeats the rule with row-level security on each pooled connection.
  • One codebase for iOS, Android and web; the desktop app is the web export inside a Tauri shell.
  • A change to the schema or to the API has to break the build, not the running app: SQL is checked at compile time and the client types are generated from the OpenAPI document.

Architecture

Three tiers: the client only presents, the API authenticates, validates and aggregates, the database is the source of truth. The API is a Cargo workspace of layered crates: each depends only on the ones below it, and CI checks that the domain stays free of I/O.

  1. yourf-api

    HTTP: thin routes, authentication, error mapping, OpenAPI.

    yourf-worker

    Background jobs: push notifications, alert digest, recurring movements, balance reconciliation.

  2. yourf-runtime

    What the two binaries share: configuration, telemetry, lifecycle.

  3. yourf-db

    SQLx repositories, row-level security scope, writes in one transaction.

    yourf-import

    Readers of other apps' files into a neutral format, with no database.

  4. yourf-domain

    Models, contracts and business rules. No I/O.

Two binaries, one image: API and worker deploy and scale separately.

One request, end to end

Creating a movement:

  1. 01Client

    The form validates the draft and sends it with the bearer token.

  2. 02API

    It authenticates the token, loads the user, validates the body and inserts the movement with its account and category splits in one transaction.

  3. 03Database

    Deferred triggers check that the splits add up and that accounts, categories and tags belong to the user, then update the balances. A violation rolls everything back and comes out as a 400 with a stable rule code.

  4. 04Client

    On success the lists, the totals and the dashboard reload.

Decisions and trade-offs

  1. 01

    Integrity lives in the database

    Balances, data ownership and category rules are enforced by triggers. It means writing logic in SQL instead of Rust; in return no client and no handler can corrupt the data.

  2. 02

    Checkable balances, not a full double-entry ledger

    Every effect on a balance is a signed posting with a single definition, used both by the trigger that writes the balances and by the job that reconciles them. A full ledger would be worth the rewrite only if a feature needed a balance sheet.

  3. 03

    A transfer is one movement with two amounts

    What left the source account and what reached the destination. The difference covers a currency exchange as well as a fee, which therefore does not show up as an expense in the statistics.

  4. 04

    No job queue

    The worker claims the due rows with FOR UPDATE SKIP LOCKED and the push outbox is woken by LISTEN/NOTIFY. Every sweep is idempotent: running it twice, or on several replicas, gives the same result.

  5. 05

    Statistics computed on every request

    Dashboard and statistics aggregate the movements in SQL each time: no pre-computed aggregates and no cache, so the figures are never stale. The price is that the queries have to be fast on their own.

Measured

Profiling the analytics queries showed two of them growing with the whole history instead of the selected period. With the generic plan of a prepared statement, the planner chose a nested loop that re-ran a join once per movement. Indexes were not the problem: the fix was rewriting the view so that each branch starts from the movement.

Demo profile, one year of data, generic plans, measured with EXPLAIN ANALYZE as the application role.
QueryBeforeAfter
Daily flows (statistics trend)daily_flows64.5 ms1.9 ms
Essential expenses (dashboard summary)essential_expenses48.0 ms1.5 ms

The app

yourFinance dashboard on desktop: net worth, FIRE number, savings rate, emergency fund and a chart of net worth over twelve months.
Dashboard: six metrics, net worth over twelve months, asset allocation. Demo data.
Statistics screen: income, expenses, net and savings rate for the last twelve months, with a monthly bar chart.
Statistics: income and expenses by month, with filters and a breakdown by category, account or tag. Demo data.
Movements list on a phone: totals of income and expenses, then one card per movement.
The same code on a phone: the movements list. Demo data.

What comes next

  • An expiry date on accounts, with a notice before it arrives.
  • Amortisation: a cost spread over several months in the statistics, while the balance stays cash-based.
  • An assistant that answers questions about statistics and accounts through read-only tools: the model picks the tool, the backend computes every number.

Code and demo

The repository is private. On request I walk through the code and the running app.

Get in touchBack to Systems