The Challenge
Saving for a wedding as a couple raises the same questions every week: how much is in, who put in what, and are we on pace for the date? A spreadsheet answers none of them reliably. I wanted an app where deposits aren't typed in by hand at all — every one is a real payment — and where nobody ever has to chase the other person for an update.
My Approach
Everything runs on one Cloudflare Worker (Hono) with D1, serving the Vue SPA, the JSON API, two webhooks and the cron jobs. Money paths are defensive by default. The payment provider doesn't sign its webhooks, so the webhook path carries a random secret and every notification is verified back against the provider's transaction-status API before a single rupiah is added. A sandbox/live mode guard rejects mismatches in both directions, and one-way status transitions mean a webhook delivered twice never counts twice.
Login is a 4-digit code sent over Telegram: it burns after five wrong tries, expires in five minutes, is rate-limited per username, and the response is identical whether a username exists or not. Sessions store only SHA-256 hashes. Notifications — deposits, 25/50/75/100% milestones, a monthly summary and an evening reminder — are claimed with INSERT OR IGNORE, so each is sent exactly once. The bot's command handlers are pure functions, portable off Cloudflare without a rewrite.
The Results
In production with live QRIS since September 2026, with 247 tests (207 worker, 40 client) written test-first. A separate demo environment with its own database and read-only guard lets anyone try the full flow without touching real savings.