ChillCheck — the temperature log that gets filled in
What inspired it
In most US counties, the result of a health inspection is public. A score or a letter grade goes up in the window, and it moves revenue. The first thing the inspector asks for is the temperature log: proof that the walk-in held at 41°F or below, that the soup cooled fast enough, that somebody actually checked.
Almost every small restaurant still keeps that record on a clipboard by the walk-in door. The clipboard has exactly one failure mode, and it is not inaccuracy — it is that nobody writes on it. A Tuesday lunch rush does not pause for paperwork. By Friday there are holes, and holes are the thing an inspector notices.
So the problem was never measuring temperature. Thermometers work. The problem is that the record has to exist every day, without anyone having to remember.
That reframing decided the entire product. The notification design is the product; the features come second. If the reminders get ignored, nothing else I build matters.
What I built
ChillCheck is a Flutter app for one small kitchen. You register your units — walk-in, freezer, hot-hold — set a target range and the times each gets checked, and the phone asks at those times. The notification opens straight onto a number pad for that unit. Type the number, done.
When a reading falls outside the range you set, it asks what you did — discarded, moved, adjusted the thermostat — with room for a note and a photo. At the end of the month you export a PDF and hand it over.
Four decisions shaped it more than the feature list did.
No server, no account, no sign-in. The kitchen data never leaves the phone. This was a constraint I set before writing any code, and it removed entire categories of work: no auth, no sync conflicts, no breach surface, no GDPR representative. It also means the app works in a walk-in with no signal, which is where it is actually used.
Never say the food is safe. ChillCheck records what you measured against the range you configured and prompts you to record what you did. It does not certify compliance and never claims a product is safe to serve. That line is enforced in the copy, the PDF, and the onboarding — and it is the difference between a record-keeping tool and a liability.
Honesty is the product. Backfilling a missed day is essential — without it
people give up and go back to paper — but a late entry must never be able to
pretend it was on time. enteredLate is derived in exactly one place, and there
is no parameter, anywhere, that can force it false. Later I added the ability to
correct a mistyped value, and applied the same rule: the correction stamps
correctedAt and shows in the report, the same way a paper log is corrected with
a line and an initial rather than an eraser.
At most four notifications a day. The moment they get ignored, the product is dead.
The cooling timer, and a constraint that is easy to get wrong
Cooling cooked food is where kitchens actually fail inspections, and the FDA rule has two stages:
$$135^\circ\mathrm{F} \;\xrightarrow{\;\le\,2\text{ h}\;}\; 70^\circ\mathrm{F} \;\xrightarrow{\;\le\,4\text{ h}\;}\; 41^\circ\mathrm{F}$$
The naive model tracks the total and checks $t_1 + t_2 \le 6$. That is wrong, and wrong in the direction that matters: a batch that took three hours to reach 70°F has already failed, and no amount of hurrying the second stage repairs it. The real constraint is a conjunction:
$$t_1 \le 2 \quad\wedge\quad t_1 + t_2 \le 6$$
So the timer computes both deadlines from the moment you start a lot, and warns before each one — not after.
Temperatures are stored in Fahrenheit and converted for display, $^\circ\mathrm{C} = (^\circ\mathrm{F} - 32)\cdot\tfrac{5}{9}$, so switching units changes what you read and never what is on record.
Where the money fits
Subscriptions are RevenueCat, with one requirement that shaped the implementation: a paying kitchen with no signal must never be locked out of its own logbook. So the entitlement is mirrored into local storage and that is the source of truth for gating. A network failure can never take away access somebody has paid for.
I hand-built the paywall in Flutter rather than using a hosted one, which means owning App Review guideline 3.1.2 myself — each plan's title, duration and price, links to terms and privacy, the renewal language. Those are pinned by widget tests, because they are invisible until a reviewer rejects you for missing one. Prices are never hardcoded; they always come from the store.
What I learned
Stores fail by going quiet, not by erroring. The paywall spun forever on Android. The logs showed Google Play Billing repeating "starting connection" and simply never answering — because the app was not yet on a testing track. It never returned an error to time out on. Now every store call has a ceiling, because a spinner with no end reads as a broken app rather than a store that is not ready.
Silent limits are the dangerous ones. Android's Auto Backup stores nothing at all once an app exceeds 25 MB, and tells you nothing. So the automatic backup carries only the log — small forever — while photos ride along in the explicit export and in device-to-device transfer, which has no such cap.
A share sheet is not a save dialog. I shipped "back up your records" through the share sheet and got a row of third-party document apps with no plain "save here". The document picker is the OS's actual save UI, and it lists iCloud Drive and Google Drive without the app needing an account with either — which is how a no-account app offers cloud backup at all.
Privacy labels are stricter than intuition. I was going to file "Data Not Collected", which felt true: nothing the kitchen types leaves the phone. It is wrong. What an embedded SDK receives counts as collection by you, and the purchase receipt goes to RevenueCat. The honest declaration is Purchases → Purchase History, not linked to identity, not used for tracking.
Typography is a correctness problem. A dagger marking a late entry, set
immediately after the number the way footnotes are set in prose, reads as part of
the value in a column of figures — 38† looks like a unit. In a numeric table
the marks belong beside the figures, not against them.
The hardest part
Not the code. It was deciding, repeatedly, what this app is not allowed to do — not judge safety, not let a late entry look punctual, not quietly rewrite a number, not lock a paying kitchen out when the signal drops. A record is only worth keeping if it can be trusted, and every one of those would have been easier to allow than to forbid.
Log in or sign up for Devpost to join the conversation.