Munchable keeps a log of everything you have scanned, and lets you say how you felt afterwards. It is the one place in the app where the user writes about themselves. It is also, by any sensible reading, health data about a diagnosed condition, which is a special category under UK and EU law.
The simplest way to protect that is not to hold it. So the log never leaves the phone, and the consequences of that one sentence shaped the whole feature. Our own privacy policy ends up saying things most privacy policies do not get to say, including the uncomfortable half:
We cannot read any of it, we cannot recover it for you if you lose your device, and there is nothing in it for us to hand over if we were ever asked for it.
The GDPR export has to apologise for itself
Munchable has a data export, because a right of access is not optional. The route that serves it carries a comment, and the comment is load-bearing:
// (conditions, sensitivities, scan history) lives only on the device and never // reaches our servers, so it is deliberately not part of this export. and the JSON it returns carries the same thing in a field, because a user who downloads their data and finds no history is entitled to an explanation rather than a guess:
note: 'Your conditions, sensitivities, and scan history are stored only on your device and are never sent to our servers, so they are not part of this export.', This is the part of data minimisation nobody warns you about. Holding less is not only cheaper and safer, it also obliges you to write prose about the hole it leaves, in the one document people read when they are already suspicious of you.
Three feelings, and no fourth
The entire vocabulary of the symptom half is this:
export type Feeling = 'fine' | 'off' | 'rough'; Fine, a bit off, rough. Plus one free text line capped at 140 characters, with a comment under the constant saying the note is one line and not a diary page.
Three was not a UI convenience. A scale of ten invites precision the user does not have about their own gut, and the moment you have ten points somebody expects a chart. Three buttons are a thing you can tap in a supermarket aisle, hours after eating, truthfully.
The entry itself is seven fields:
export interface HistoryEntry { id: string; barcode: string; name: string; brand?: string; verdict: VerdictValue; at: number; feeling?: Feeling; note?: string; } The verdict is stored, not recomputed. It is the verdict at the time of the check, so the log reads as it was rather than as it would be now. Rules get refined, and a list that silently rewrote its own past would be the one thing in the app you could not trust.
A rescan in the aisle is not a second entry
The one non-obvious function in the module is append:
export const REPEAT_WINDOW_MS = 60 * 60 * 1000; Scanning the same pack twice inside an hour is one check, not two: a rescan at the shelf, a tap on the recent row, a capture that replaced a miss. The entry refreshes in place and keeps whatever feeling was already attached to it. That last clause has its own test, with a name longer than the code it checks, because losing a note somebody typed about their own symptoms to a tidy-up in the data layer is not a bug you get to apologise for twice:
test('appendEntry: a rescan of the same pack inside the window refreshes rather than duplicates, and keeps the feeling', ...) Eight tests cover the module in total. They are pure functions with a clock passed in, so there is no fake timer anywhere: appendEntry, recentUnique, dayLabel, groupByDay, each handed T0 and asked what it produced.
Days are grouped by local calendar day, with Today and Yesterday as labels and the year appearing only when it differs from the current one. A check at 23:30 files under the day it happened, which sounds obvious until you build day boundaries out of UTC timestamps and someone's late snack shows up tomorrow.
The cap is 500 entries, and the comment on it says what kind of number it is: a year of daily scanning fits, the screen renders it all without paging, so it is a cap on a scroll rather than on a dataset.
The feature we did not build
Here is the thing a food log plus a symptom log is obviously for: correlating them. You scanned fourteen things with a given ingredient, you felt rough after nine of them, here is your pattern.
Munchable does not do that, anywhere. The screen's own doc comment is the policy:
It is a plain record: the verdict says what the product was, the feeling says how the day went, and the screen draws no conclusion from the two. Whatever pattern is there is the reader's to see.
Three reasons, in increasing order of how much they settled it. A correlation over a handful of entries is noise, and dressing noise up as a finding is how a wellness app starts telling people things that are not true. The app has no idea what else somebody ate, which is most of the variance. And a tool that is careful to say it is not medical advice cannot then hand you a diagnosis-shaped sentence about your own body. The FAQ on the homepage is explicit that this is a wellness tool and not a diagnosis, and a correlation engine would quietly make that untrue.
So the log is a record, and the interpretation is the user's, or their dietitian's. There is an export-shaped hole here I am aware of: the useful next step for somebody with a real pattern is showing the list to a clinician, and right now that means handing over a phone. That one is worth building. An automatic verdict about their body is not.
Small UI decision that came out of the same thinking
On the history screen, one entry's feeling row is open at a time. The comment explains why, and it is the smallest version of the whole argument:
One at a time: the chips are the same three on every row, and a page of open rows is a form, not a log.
If you want to see the architecture this hangs off, the privacy policy is the honest version: health data on the device, verdicts computed there, a barcode lookup that carries a barcode and nothing about you. The conditions the app models are at munchable.app/conditions, and the ingredient guides at munchable.app/answers are the part that works without installing anything.

