Building an offline first finance app with Flutter, Drift and Riverpod
Spendly keeps every transaction on the phone, with no account and no server. This is the stack behind it and why we chose a local database over the cloud.
Spendly is a personal finance tracker that works without an account and without a connection. Every transaction, category and budget lives in a database on the phone. That was a product decision first: people are more willing to log money honestly when they know it never leaves the device.

Drift for a real, typed database
We use Drift, a type safe layer over SQLite. Tables are Dart classes, queries are checked at compile time, and migrations are explicit. For a finance app, where a broken migration means lost data, that matters more than anything else.
Drift generates code, so after changing a table you regenerate it:
dart run build_runner build --delete-conflicting-outputs
The generated file is not checked in, which keeps diffs clean but means a fresh clone needs this step before it will compile.
Streams all the way to the UI
The nicest part of Drift is that any query can be watched. Instead of fetching data and refreshing by hand, a query returns a stream that emits again whenever the underlying rows change.
Riverpod's StreamProvider wraps those streams. When a user adds an expense from a bottom sheet, the home total, the budget bars and the reports screen all update by themselves, because they are all watching the same tables.
What offline first changes
- No loading states for your own data. Everything is local, so screens open instantly.
- Backups become the user's job. With no cloud copy, we are clear in the app and the privacy policy that uninstalling deletes the data.
- Privacy is simple to explain. There is nothing to collect, so the data safety form is short and honest.
Would we do it again?
Yes. For a single user tool like this, a local database is faster, cheaper to run and easier to trust than a backend. If Spendly ever adds sync, Drift's typed tables make it straightforward to mirror the same schema in Postgres.
