Products
Tally
Personal finance without the lecture — where the money actually went, drawn as one honest chart.
The brief
Most personal-finance apps do one of two things: they connect to your bank and become a privacy problem, or they present a dashboard of nine widgets that answers every question except the one you opened the app to ask — where did it go?
Tally is the narrow version. One month at a time, one chart, and a straight answer.
The constraints
- Financial records are the most sensitive data a small app can hold. Anything stored on a server had to be unreadable by whoever runs the server — including me.
- Nobody sees a figure they have not signed in for. Not for a second, not on a bad connection, not on a shared laptop.
- Two devices must not silently destroy each other's edits. Losing a week of someone's records to a background sync is unforgivable in a way that losing a to-do item is not.
The decisions
The ledger is encrypted in the browser before it leaves
The server stores an opaque blob and an email address. It cannot read a single transaction, because the key is derived on the device and never sent. That is a real constraint rather than a marketing line: features that would need server-side inspection of the data simply cannot exist.
What it costs: no server-side search, no cross-user insights, and a forgotten password means genuinely unrecoverable data. Worth it for this category.
The app fails closed — a decision I got wrong first
The original build was local-first: it probed for a backend on boot, and if it got no answer it dropped the sign-in screen and worked entirely on the device. The reasoning was that nobody should be locked out of records stored in their own browser. It reads well, and it was the wrong call.
Local-first meant the app rendered before it knew who was reading. On a throttled connection the ledger sat on screen for a second and a half before the sign-in screen arrived on top of it. Worse, any failed probe — no network, a bad gateway, a stale service worker — removed the sign-in screen altogether and opened the app. A flaky connection was enough to hand someone an unlocked ledger.
Now the sign-in screen is part of the document rather than something a script reveals, so it is up before the first line of JavaScript runs. The app boots with nothing in it; a ledger is only read once an account has named the store to read it from, and it is removed from the device on sign out. When the server cannot say who you are, the app says so and stays shut.
What it costs: no offline first run, and no trying it without signing up. For a to-do list that would be too high a price. For someone's money it is the only defensible direction, and the failure it prevents is one nobody would ever report — it looks like the app working.
Sync refuses stale writes instead of resolving them cleverly
Every write carries the version it was based on. A write from an out-of-date device is refused with the current copy attached, and the client decides what happens next. No last-write-wins, no silent merge.
What it costs: the client has to handle a conflict path that most apps skip. That path is the difference between a sync bug and a lost month.
One chart, drawn by hand on a canvas
Cashflow across the month is a single line over a filled area, with the running balance above it and the category breakdown below. No charting library — the chart is a few dozen lines against a canvas, so the page carries no dependency and nothing to update.
What it costs: no free tooltips, legends or zooming. Everything the chart does had to be written, which kept the feature list honest.
Built with
- Vanilla JS, no framework
- Canvas
- Web Crypto
- Cloudflare Pages
- Pages Functions
- D1 (SQLite)
- PWA