Products

Evidence

A habit tracker built on identity rather than streaks. Accounts, offline-first, ten languages.

The Evidence landing page: the headline You don’t have habits, you have evidence, beside a phone showing habits being built and broken on one screen.

The brief

Habit trackers mostly sell the same thing: a grid of boxes, a streak counter, and the hope that guilt will do the rest. It works until the day you miss — and then the streak resets to zero, the app tells you that you have nothing, and you delete it.

I wanted to build the version that survives the miss. The organising idea is identity rather than repetition: you don't have habits, you have evidence of who you're becoming. You name the person you're trying to be, and every check-in is a piece of proof. Miss a day and you still have all the previous proof, because evidence doesn't reset.

The same frame handles the other half of the problem. Building a good habit and breaking a bad one are the same act seen from two sides, so they live on the same screen instead of in two different apps.

The constraints

  • Free hosting, with a real CPU ceiling. The whole thing runs on Cloudflare's free tier, where a request gets roughly ten milliseconds of CPU. That is not a footnote — it dictated the authentication design.
  • A tap can never wait on the network. Checking off a habit is the core interaction and it happens on a phone, often on bad signal. If it ever spins, the app has failed at the only thing it does.
  • Accounts required. Local-only storage is easier and I chose against it: losing a year of someone's record because they changed phones is not an acceptable failure.
  • Ten languages, with no translation budget. The interface had to work in scripts and word-lengths I can't eyeball, which rules out layouts that depend on text being short.

The decisions

The browser derives the auth token; the server only salts and digests it

The obvious way to handle passwords is a key-derivation function on the server. On this platform that overruns the free plan's CPU budget and the request dies with an opaque error — which is exactly what happened the first time.

So the work moved to the client. The browser derives a token from the password, and the server salts and digests what it receives. The password itself never travels and never lands in a log.

What it costs: the derivation is only as strong as the device running it, and a client that lies is a client that gets in. It is the right trade at this scale and would need revisiting the moment anything sensitive lived in the account.

Every write lands locally first, then syncs

Tapping a habit writes to local storage and re-renders immediately. Synchronising happens afterwards, out of the way. The interface never shows a spinner for the one action people came to perform.

What it costs: two devices editing the same day can disagree, so the sync has to reconcile rather than overwrite. More code than a naive "save on the server" — and the reason a subway ride doesn't break the app.

Schedules are daily or specific weekdays. Nothing else.

"Three times a week" is an obvious feature and it is not in the app. Streaks for it need week-based arithmetic that the rest of the model doesn't have, and a streak that quietly computes the wrong number is worse than a missing option — it teaches you to distrust everything else on the screen.

What it costs: a real use case is unserved until the underlying model earns it. I would rather ship fewer options than a number I can't defend.

Six palettes; fixed layout and type

Themes are the most-requested personalisation and the easiest way to wreck a design. Colour is user-selectable — three light, three dark — while spacing, layout and typography are not. The app can look like yours without being able to look broken.

What it costs: less expressive than full theming. It also caught a real bug: a colour transition on body left text at 1.07:1 contrast during a live palette swap, which only showed up by driving the actual interface.

A slip is logged, not punished

For habits you're quitting, the clean-time counter is tappable: you log the slip, and the app records it as data rather than treating it as failure. The counter restarts; the history doesn't disappear. Relapse is part of quitting, and software that pretends otherwise gets deleted on the first bad day.

What it costs: it makes the headline number less flattering. That is the point.

What it taught me

A green test suite is not evidence that software works. Every significant defect here was found by driving the real interface, not by the tests: the database returned schedules as JSON text, so streak logic silently treated every habit as daily; a write path was missing a tenant check, so one account could reach another's rows; and a cached service worker could pin a user to a broken version indefinitely.

That last one is why every deploy stamps the service-worker cache. A PWA that can strand users on old code is a PWA that can't be fixed remotely — the worst failure mode an offline-first app has.

Built with

  • Vanilla JS, no framework
  • Cloudflare Pages
  • Pages Functions
  • D1 (SQLite)
  • Web Crypto
  • Service worker / PWA
  • Vitest

No build step and no framework: the browser is served the files as they were written. It keeps the payload small, and it means there is no toolchain to rot between now and the next time this needs changing.