← All work
Case study

Sole Ledger

The bookkeeping app I built to run my own company across two tax systems: time, expenses, invoices and a live tax reserve, with no accountant in the day-to-day loop.

Sole Ledger screenshot
Role
Everything: product, the cross-border tax model, Flutter app
Stack
Flutter · Dart · Riverpod · Drift (SQLite) · pdf · ML Kit
Ships as
Android + macOS desktop + web, from one codebase
Data
Offline-first, single-file SQLite, synced with Syncthing. No server
Now at
v1.4.0, database schema v11 — still the app the books are kept in
How it came together
  1. 01

    Why I built it

    I run a Dutch eenmanszaak while living as a tax resident in Japan, invoicing EU business clients. That combination makes 'do your own books' awkward: two tax systems, two currencies, and a VAT treatment that isn't the one most people assume.

    Every off-the-shelf tool solved one slice and left me stitching the rest together by hand each month. I wanted one app that took the recurring friction out of logging time, capturing expenses, cutting invoices and knowing what to set aside, so that running the business stopped eating the hours I'm paid for.

  2. 02

    The build

    One Flutter codebase to Android, macOS and web, offline-first on a local SQLite database. Money is never a float: integer minor units throughout, and tax rounds once per rate group. Time entries flow into invoices: pick a client, and unbilled hours pull in as lines. Those render to a tri-lingual PDF with the correct legal wording for that client's VAT case.

    Expenses are a tap and a photo. On-device OCR pre-fills amount, tax, date and currency for me to confirm; it never auto-saves, because they're my books. A fixed-asset register handles depreciation properly and feeds the right yearly slice into deductions. A dashboard card runs year-to-date profit through a Japanese income-tax estimate, so I always know the reserve.

    Most of what has been added since is the unglamorous half that only shows up once a tool is actually in use. Invoices are editable rather than final: a draft can be refreshed to pull in hours logged since it was raised, due dates are optional, and a two-party signature block takes a signature drawn once and kept. Reports do monthly timesheets as well as quarterly ones, because that is what a client asked to be sent. Records can be selected and deleted in bulk, and a renamed client is renamed everywhere it is shown. Eleven schema migrations in, every one of them runs against a test that upgrades a real ledger.

  3. 03

    What was hard

    The load-bearing part wasn't code, it was the tax model. When a client says 'add tax on top' they mean VAT, not my income tax, and for a Japan-based supplier billing EU businesses that resolves to a 0% reverse-charge invoice, with income tax settled separately in Japan. Getting that right, and having the app teach it rather than quietly do the wrong thing, drove the whole invoicing design.

    The other half was trust and portability. The entire ledger is one SQLite file, so Syncthing keeps laptop and phone in step with no server. That only worked after getting the database path right per platform, and being honest about where it can't work at all (the web build's sandboxed storage).

In screenshots

What this means for your project

Product engineering in a domain with unforgiving rules: a cross-border tax model, offline-first data I trust with my own money, three platforms from one codebase, and the dozens of small friction-removing decisions that make a tool something I open weekly instead of a demo I abandoned.

FlutterDartRiverpodDrift (SQLite)pdf / printingML Kit OCRgen-l10n