Case study

Taskr

Splits a household's chores by the time each person has.

Role
Sole developer of the rebuild: the charter, the design, the code, and the migrations applied to the live project.
Stack
React as an installable web app, Supabase Postgres with row-level security and column grants, Vercel, and an allocation module in plain JavaScript with no React in it.
Timeframe
A five-person classroom project in 2020, rebuilt from a charter since August 2026. Still in progress.
Links
The app · How to start testing it · Source on GitHub
Taskr's split for a demo household of three under the verdict 'The split is level.': Alex at 80 of 150 minutes, Robin and Sam at 55 of 100 each, and their three bars at the same height.

The problem

Households divide chores by habit, by nagging, or by an even split that is not even. Give a parent with 300 free minutes a week and a child with 60 the same number of chores, and the child’s week is gone while the parent’s has barely started. A count is the wrong unit. So is a total of minutes: what has to match is how much of each person’s own week the work takes.

Taskr began in 2020 as a five-person classroom project with that idea at its centre and a view with one progress bar per person. It ran as desktop pages against localhost, over a database a startup script regenerated, so it had no deployment, no persistence and no phone. The rebuild’s charter kept the idea and set the bar at phone-usable for a real household, which the old architecture failed by its structure rather than by degree. So it was rebuilt, not refactored.

Fairness by time has a sharp edge, too. When somebody’s week is close to the size of one chore, no arrangement of the work is fair, and an app that divides it anyway has to decide what to tell the household.

What I built

An installable web app for a household’s phones. Each person is a budget of minutes for the week and each chore a number of expected minutes with a due date, and the app opens on the split: every person’s load drawn as a share of their own capacity. A parent at 150 of 300 minutes and a child at 30 of 60 are level, and the bars show it without a number being read. Members stay in roster order and are never sorted by load, because ranking people by output is the exact inversion of what the app is for.

The allocator is a pure module with no React and no database in it. It is handed each person’s minutes for the week rather than reading them, so capacity is a fact about a week and not about a person, and an override of zero minutes wins over the baseline instead of being mistaken for a missing value. It is judged against a corpus of thirteen household shapes whose expected outcomes were worked out by hand before the module ran against any of them, because expectations generated by the code under test only prove the code still does what it did.

Its answer is written, not only drawn. When somebody’s week changes, the household’s open chores re-assign with nobody pressing anything, and a chore a person placed by hand stays where they put it. At setup, when nobody holds anything yet, one control, Deal these out, hands out the unheld work. There is no re-balance button: the charter ruled one out, because a button someone has to press is the negotiation moved rather than removed.

Re-running an allocator from scratch moves most of the list, which is fair and infuriating. Over the corpus with each household’s largest capacity halved, a re-balance with no stability rule moved 25 of 43 held jobs and 1,240 minutes. Preferring whoever already holds a job when two placements tie, plus a budget of 120 minutes of movement, brings that to 11 jobs and 355 minutes.

The rest is where trust has to live. Row-level security scopes every row to its household, and column grants decide which columns a client may read or write, since a row policy says nothing about columns. The moment a chore is done is stamped by the database’s clock, so a phone set to the wrong date cannot move work between weeks. The build refuses a key shaped like a secret in any variable that gets inlined into the client, a check added after one reached a preview bundle in August. Every migration is replayed in CI against an in-process Postgres. The live suite that tested the policies over the wire, as a client would, ran until September, when retiring the organizer’s password mint broke its fixture; rebuilding it is Taskr #436.

Shipping is two acts in an order somebody chooses. Production builds from a release branch that only a pull request I merge can enter, and a migration is applied to the live database before the client that needs it is promoted. The order exists because it once ran the other way: in August a merge was a deploy, client code went live ahead of two migrations nobody had applied, and for a day the live app could not hold a household at all.

Where two phones can race, the race is run rather than reasoned about. Two clients finishing the same shopping run at the same instant, ten repetitions a run against the live project, produced one winner and one refusal forty times out of forty.

A decision I’d defend

When a fair split is out of reach, the split says so and says why. It never rounds until the bars look level.

Level has a number. The split counts as level when the highest and lowest shares of their own capacity are at most 10 percentage points apart. Outside that, the verdict above the bars says how many minutes off level the household is and who is carrying the extra. When no arrangement can reach level, it says that instead, and names the person and the two figures that make it arithmetic: their fair share and the smallest job on the list.

The case that forced it is small. A household with 25, 100 and 150 minutes this week and jobs of 40, 35, 30, 30, 25, 20 and 10 minutes sits near 69% of its capacity, so the person with 25 minutes should carry about 17. The smallest job is 10. They land on 40% or 80%, and no arrangement puts them near 69%. The prototype hit exactly this before the rebuild began: a greedy allocator was ragged in 3 of 5 scenarios, and a local-search pass fixed two of them and could not fix this one.

The same demo household after Sam's week drops to 25 minutes. The verdict reads 3 min off level, and that level cannot be reached this week because Sam's fair share is 17 min and the smallest job is 10 min. A line below says the re-balance moved 120 min and the change budget held the rest. Alex carries 105 of 150 minutes, Robin 65 of 100, Sam 20 of 25.

So the honest answer had to be the product’s answer. An allocator that reports level over a visibly ragged set spends the one number the whole app rests on, and the households it would mislead are the ones it was built for: a child’s week, or somebody’s short one, is where a budget comes close to the size of a single job. Naming the reason is what makes the verdict read as the fairness claim holding up rather than as the app failing.

What it rejected, and why:

The cost is stated rather than hidden. Level is a band, so two people up to 10 points apart both read as level. The allocator is greedy, so when it places a job it cannot see the final spread. In the stability corpus that cost one shape its levelness once ties went to the incumbent, and it makes the movement budget non-monotone. A budget of 0 minutes reaches level in 2 of the 10 contested shapes, 30 and 60 minutes in 1, and 120 in 2 again. And the split counts only time spent doing the work. Noticing, planning and remembering it are work the app does not count, and the split screen says so.

Outcome

Open for testing at taskr.madcowhq.com, where anybody can start a household. As recorded in the repo on 2026-08-08 and read from it on 2026-09-14, the allocator reaches level in 10 of its 13 corpus shapes (76.9%), and in 7 of the 10 (70.0%) where level is a real question. Both figures are given because three shapes have fewer than two people with any minutes, and a set that small is level by arithmetic rather than by anything the allocator did.

The 2020 code is kept at a tag, and none of it is on the branch that ships. The charter’s test for every proposal in the rebuild was whether a screenshot of the old one-bar-per-person view could satisfy it; if one could, the proposal had failed.

What is not proven yet is the part only a real household can settle: whether people accept a split nobody negotiated, week after week. That has its own epic in the tracker, and until it is read this page makes no claim about it. Still in progress.

Households wanting to try Taskr rather than read about it are better served by the tester page, which walks through the first steps.