-
-
The app icon: one plate, one form resting off-centre. The mark is the product thesis.
-
The result screen as the app draws it, with no frame or composition added.
-
One addition that makes the meal hold you. Live on the App Store and Google Play from a single Compose Multiplatform codebase.
-
Type the meal, get up to three additions sorted by effort. Common foods answer instantly and offline, with no network call at all.
-
The same meal, answered for where you actually are. No kitchen means nothing that asks you to cook.
-
Tap Add and the form lands on the plate. Nothing is logged, and there is no diary to come back to.
-
For days when eating is hard: one addition, and the app says that is enough instead of pushing for a third.
-
Constraints the build enforces, not just the copy: it only ever adds, and never shows a number that reads as a budget.
Inspiration
Advice about eating better arrives in two shapes: track everything, or cook something else. Neither helps at nine in the evening when you are already holding a packet of instant noodles. The question people actually have in that moment is smaller and more answerable — what one thing would make this hold me until morning?
So the app answers only that. It never asks you to remove, swap or reduce anything. There is no domain model for subtraction, and there never will be.
What it does
Type what you are about to eat. You get a plain-English read on what the meal is missing and up to three things to add, sorted by effort, chosen for where you are and what you keep in your kitchen.
- Instant noodles → crack an egg in, throw in frozen peas.
- Toast → peanut butter, a boiled egg on the side.
- The same meal in an airport gives different answers, because nothing there asks you to cook.
Two of three components is the target, and a meal that already reaches two is told so rather than pushed toward a third. Tap "not for me" and that ingredient never appears again. Tap Add and the combination is saved, ready to repeat.
There are no calories, no macros, no weights, no daily totals, no streak to break, and nothing to review later. Say a meal already covers you and the app agrees and stops talking.
How we built it
One Compose Multiplatform codebase for iOS and Android. 91% of application code is shared by
line — 9,354 lines in commonMain against 440 Android, 298 iOS Kotlin and 227 Swift — and all 36
UI and ViewModel files are shared. Platform code is limited to app entry, haptics, notification
permission, URL opening, the push bridge, the HTTP engine and the launch screen.
Two architectural rules did most of the work:
- Dependency injection instead of
expect/actual. Every platform capability is an interface injected with Koin, so each one can be faked incommonTestwith no mocking framework. - Nothing throws across the Kotlin/Swift boundary. An uncaught exception reaching Swift kills an iOS app instantly, with no usable trace, so every fallible operation returns a sealed result that maps to designed copy.
The suggestions come from an authored catalogue bundled in the app: 664 meals, 123 additions, 84 ingredients. Allergen data is written by hand, never generated, and excluded from the candidate set before anything ranks it, so a flagged allergen is structurally unreachable rather than filtered afterwards. Missing data counts as unknown and fails closed. An LLM tier and a Cloudflare Worker exist in the repository but are switched off in the shipped builds: everything you see answers on-device, in under a frame, with no network at all.
Purchases run through RevenueCat's Kotlin Multiplatform SDK on both stores. Push runs through a wrapper we wrote and published as its own Apache-2.0 library.
241 unit tests, plus 28 design guards that fail the build — not lint rules, but the product's own constraints written down as code: no digits in a suggestion, no banned vocabulary, no shadows, no advertising ID, no ViewModel coroutine without an exception handler.
Challenges we ran into
The paywall opened underneath the iOS system alert. We asked for notification permission and raised the upgrade sheet in the same breath. On iOS the permission prompt is asynchronous, so the sheet arrived first and sat behind it. Found while recording the demo video, fixed by making the permission call report its answer so the sheet waits.
A push told people to try something they had just done. Tags were written once per launch, so the "has completed a check" flag stayed false until the app was next opened — and the permission prompt appears seconds after that first check. Our activation journey, which enters two hours after a first session, therefore nudged people who had already used the app. The dashboard said it plainly: 36 delivered, zero clicks, five unsubscribes. We stopped the journey, wrote the tag at the moment the check completes, and added a guard so it cannot regress.
Google Play promo codes do not grant a subscription. They attach the offer to the account and apply it when the person subscribes, which is the opposite of Apple's offer codes. We only learned this by redeeming our own codes and hitting the paywall. Our instructions for reviewers said the wrong thing and would have made working codes look broken.
Accomplishments that we're proud of
Live on both stores, through three review cycles, with two real defects caught and shipped rather than discovered by users. 91% shared code including every screen. A push wrapper extracted and published for anyone else building on Kotlin Multiplatform. And a set of guards that make the product's promises — it only ever adds, it never shows a number that reads as a budget — into things the build enforces rather than things we remember.
What we learned
Rules survive only if a script enforces them. Every guard in this repository exists because a written rule was broken anyway.
Store mechanics differ in ways no test suite can catch. The only way to find out what a reviewer will experience is to redeem your own codes on your own phone.
Dashboard numbers are a bug report. A zero click-through with unsubscribes is not weak copy, it is the wrong message reaching the wrong people.
What's next
Direct interop for the push library on iOS so it needs no Swift shim. More authored additions, which is the only thing that widens what the app can answer. And an honest decision about the subscription before the season ends: the tiering means the app keeps working with the backend switched off entirely, and we intend to preserve that.
Log in or sign up for Devpost to join the conversation.