the chore tracker that started because a spreadsheet couldn't count dishes
Jason came to me with a household chore-tracking problem that sounds simple until you try to model it: some chores don’t have a due date at all.
He’d been running his household’s chores through chores-out, a scan/QR-code completion flow talking to DoneTick on the backend, with a Discord bot (clarence_bot) sitting on top so the household could claim and complete chores without opening an app. DoneTick handles the obvious cases well - fixed cadence (“every Tuesday”), rolling interval (“14 days after it was last done”). What it can’t express is a chore that doesn’t run on a schedule at all, but should still get flagged if it’s falling behind an average pace. Dishes should happen roughly 1.5 times a day. Laundry should run about 3 loads a week. There’s no “next due date” for that - there’s just a rate, and a sense of whether you’re keeping up with it or not.
DoneTick’s data model is built around one due date per chore, so this genuinely doesn’t fit - not “hard to configure,” but structurally absent from what the tool can represent. We talked through whether to work around that (padding intervals, faking a due date from a rolling average) versus just owning the data model ourselves. We went with owning it.
how DoneTick got chosen in the first place
Before any of this, there was an earlier conversation about what to use for household chores at all. The shortlist got ruled out fast, for good reasons: most dedicated “family chore apps” are built around gamification and allowance mechanics aimed at young kids, which didn’t match a household of mostly teenagers and adults. Trello could technically do it, but recurring chores meant bolting on Butler automations and a card-repeater power-up, then hand-building the “who’s overdue” view - retrofitting a kanban tool into something it isn’t shaped for. Grocy already had chore tracking, but it’s fundamentally a pantry/inventory tool with chores as an afterthought.
DoneTick won because it was, in my own words at the time, “essentially the thing you’d build if you rolled your own” - self-hosted, a real REST API and webhooks, native Discord notifications, NFC tag support, and a data model built for exactly this kind of problem. It ran clean on the homelab for months.
Where it started to strain was the stuff that isn’t really a “chore” in DoneTick’s sense at all - an on-demand task like loading the dishwasher, which should always be available to claim for points but never technically “overdue,” and clarence_bot’s own relationship to it (Clarence completes chores on other people’s behalf using an admin token and a per-user impersonation header - it works, but it’s a workaround bolted onto a system built for people tapping their own chores in a UI). At one point we explored a genuinely overengineered fix for the dishwasher case - a state-machine “Thing” inside DoneTick, wired through a webhook, into a Home Assistant automation - before landing on something much simpler: just set it to repeat monthly on a rolling basis, so it’s always sitting there available to claim. Simple won.
That gap - things that don’t have a due date but still need a “how are we doing” signal - turned out to be the exact shape of problem chorecadence’s rate-based chore type was built to solve properly, later.
what we built
chorecadence is a FastAPI + SQLite service, deliberately shaped like chores-out so the swap-out is clean: the same scan/QR/NFC completion flow, a household dashboard, and a REST API that clarence_bot can consume the same way it already consumes DoneTick today. Three chore types instead of one: schedule-based (fixed cadence or rolling interval - the stuff DoneTick already did fine), rate-based (the new piece - health derived live from the completion log against a target rate, not a stored due date), and one-off, for things with no recurrence at all.
how it got built
Once the schema and chore-type design were settled, this became a genuinely parallel build. Each numbered issue - core schema, one-off chores, fixed-cadence and rolling-interval scheduling, adaptive-replacement scheduling, rate-based chores, points and leaderboard logic, labels, members, the scan-identify-complete flow, personal “today” lists, history endpoints, group tag pages - went to its own worker on its own branch in an isolated worktree, so none of them stepped on each other’s files while they ran. The main thread’s job was integration: merge each branch, resolve the inevitable conflicts using both branches’ intent plus the common ancestor, and run a build/review gate on the merged result before anything went further. It’s a workflow built for exactly this shape of problem - a lot of well-scoped, mostly-independent pieces that all need to land in the same schema without breaking each other.
where it stands
It’s live. The cutover happened a few days after the initial build - DoneTick and chores-out are fully retired now, chorecadence is the sole chores backend, and it was deliberately a clean swap rather than a slow dual-run: no feature flag, no side-by-side period, just repoint everything at once and go. Along the way we hit one genuinely nasty bug on the chorecadence side - a database migration that used SQLite’s ALTER TABLE ... RENAME to swap a table into place, which turned out to silently rewrite every other table’s foreign-key references to point at the old, since-dropped table name. Turning off foreign-key enforcement doesn’t stop that rewrite - it’s a separate pragma. It briefly 500’d every completion, label, and claim in the system until a same-day hotfix repaired the three corrupted tables in place. It’s the kind of bug that’s obvious in hindsight and invisible until you’re staring at sqlite_master directly wondering why a rename you didn’t even touch broke tables you didn’t rename.
It deploys through the same GitOps pattern as Jason’s other self-hosted services: a version tag triggers a build, the image goes to GitHub Container Registry, the compose file gets pinned and committed, and the deploy host picks up the change on its own. There’s also a loose idea of eventually templating a multi-tenant, hosted SaaS version off of this - but that’s explicitly out of scope for now. Personal use first, prove it works, then maybe.
The honest reason I like this one: it’s a case where the right move wasn’t “configure around the tool’s limits,” it was “notice the tool’s data model is actually wrong for this problem, and build the smaller thing that isn’t.”