Skip to content
FamilyFolds· 7 min read

We found two P0 ledger bugs before FamilyFolds launched. Here they are.

An internal review of the FamilyFolds points ledger found that a client could mint arbitrary points, and that a negative value could break the never-negative invariant. Both were our fault, and both are the reason the reward store is not built yet.

FamilyFolds is in development, and we have labelled it that way on our website. This post explains the two findings that are the reason the reward store does not exist yet — not to excuse the delay, but because the reasoning generalises and the review itself was worth writing down.

Finding one: the client supplies the points

The approval endpoint accepted a points value from the request body. A parent approving a chore could therefore post an arbitrary number and the ledger would record it — and because the whole reward economy hangs off that balance, the impact was total.

The intent was benign. The parent approving the chore knows what it is worth, and the value came straight from the chore record. The mistake was that we trusted the caller to pass it back rather than looking it up server-side.

ts
// Before — the caller decides the balance change.
const { childId, points } = await request.json();
await ledger.append({ memberId: childId, amount: points });

// After — the server decides, from data it already owns.
const chore = await db.get(choreId);
await ledger.append({
  memberId: submission.childId,
  amount: chore.points,
});

The rule is the same one we already wrote down for Mangofold's entitlements: a client cannot raise its own tier. We simply failed to apply it to the ledger.

Finding two: negative amounts

The second was that a negative points value was accepted. That breaks the invariant the entire reward design rests on — that a balance can never go below zero and a child can never owe a debt. It also interacted badly with escrow: points held during a pending redemption could be driven negative by a negative approval.

The intended rule was that points are integers between 0 and 1000, and it was documented. It was not enforced in the type at the boundary, which is where enforcement has to be.

Why we are publishing this

  • Both were caught by an internal review, not by an external report. That is the good case.
  • Both came from trusting a caller-supplied value at a boundary. Same root cause, twice.
  • Neither would have been caught by a test suite that only exercised the happy path.

The uncomfortable part is that finding one had been written down as a principle a year earlier and finding two was its direct consequence. Knowing a rule is not the same as applying it consistently, and the only defence we have found is asserting the boundary in tests the way we do for Mangofold's row-level security.

We would rather ship FamilyFolds six months later with a ledger we trust than on time with one we do not. It is a household's savings in points; there is no version of this worth rushing.

#review#security#ledgers#process

Grallumae Technologies

Kerala

Get in touch →

[Get in touch]

Have something that needs building?

We take on a small number of contract projects — product builds, privacy-critical backend work, and taking an idea from zero to shipped. Tell us what you're working on.

Taking on select projects for 2026