A personal growth app that keeps everything on the device
Kusuo is a local-first progressive web app for a few daily habits, a set-by-set training log, and reflection. All data lives in IndexedDB through Dexie. There is no server to sign in to, and nothing leaves the device.
Habit trackers that just didn't take off
Earlier attempts at tracking habits “just didn't take off” — so the aim was low-friction daily use rather than a rich feature set. Kusuo is built for one person, and holds two halves of the same practice: a small set of daily habits, and a proper training log for the days I lift.
Success is opening it, seeing today's habits and today's session within five seconds, and coming back tomorrow — not maximising a streak or completing a feature list. It is used on an iPhone, morning and evening, and read back on a Mac.
Sole designer and developer
Requirements, product document, interface, data model, tests and deployment. 48 commits.
Decide the data model before the first table
The build brief came first — committed on 18 August, four days before any code. It fixed one writer and one reader, and an append-only log for completions, before a table existed.
PRODUCT.md sets out what the app is for and what it leaves out. It followed on 22 August, ahead of the data layer.
When three specifications later disagreed, SPEC.md was rewritten from a direct read of the code and made the authority.
Key decisions and trade-offs
An append-only event log, with one device that writes to it
Habit completions, logged sets, finished sessions, reflections and bodyweight are each a new event with its own ID; un-ticking a habit or removing a set appends another. Only the iPhone writes — the Mac has no write controls at all — so there is nothing to merge, and the log was not needed for correctness. It was chosen because it is cheap to build first and expensive to retrofit, and because it keeps history honest: un-ticking a habit is recorded, not erased. A second writer and a sync layer are planned; neither is built.
No backend, no accounts, no telemetry
The app records private reflection. A server would mean an account, a consent surface, a privacy policy and a breach to worry about — for an app used by one person. So there is no server.
What is in, what is out, and what was refused
Three gates, in order, and nothing skips one
From .github/workflows/deploy.yml — public, and quoted rather than summarised.
“Nothing reaches the phone without passing this first. The build used to be the only gate, which meant a green deploy said the code compiled and nothing more.”
The end-to-end suites run against WebKit, not headless Chromium — the engine the target iPhone actually uses. That is a choice, not a default.
Live, installable, and tested where it breaks
- A local-first PWA on React 19, TypeScript and Vite, with Dexie over IndexedDB.
- 30 unit test files, and 4 Playwright end-to-end suites — including offline behaviour and viewport scaling, the two things a local-first web app fails at first.
- A PRODUCT.md stating the scope, and the reasoning for each exclusion.
Ask the schema what tables exist, from the first table
Reset, backup and every test's setup each kept their own hand-written list of tables — six copies of the same list, none of which knew when the schema changed. Adding the tenth table left weigh-ins behind on reset while it promised to erase everything, and a backup parser with its own list could lose data with a success message.
The event log was designed so that nothing that happened is lost. The reset and the backup beside it, on a device holding the only copy, were not held to that standard until then. Now one module asks the database what tables exist; backup, reset and the tests all read from it, and a test proves every table round-trips through export and import.
This is how I make decisions at work too
Open to new roles — Toronto, hybrid, remote, or relocating.