Most “I shipped an app in N days” posts focus on the code. The framework, the deployment, the certificate dance. After actually doing it (five days to push the iOS submission of Enutri through to App Store approval), I think most of those posts point at the wrong part of the work.
The code was the easy part. The other things, in order of how expensive they actually were:
- Three Apple rejections in five days, and the strategic pivot the second one forced.
- Finding the right human to own the nutritional content, because I am not qualified to write it.
- Finding the first nutritionists and parents to test with. Neither group was in my contacts on day one.
That is the real shipping log. This is what it looked like.
Why this app exists
I am the child of African parents. I have watched, in my family and a dozen others, the gap between how parents feed children aged 0 to 24 months in our context and the digital tools available to support them. Most baby-food apps speak quinoa and small jars. Here we speak fonio, niébé, moringa, igname, gombo. None of the existing apps know what age you introduce mango. None speak French as a first language.
Enutri closes that gap. It’s published by Digitall Elevate, a Beninese SA, on three surfaces: iOS (approved), Android (live on Google Play), and the web at enutri.vercel.app. Africa first by design (pricing in FCFA where it applies, French as the primary language, content tied to local foods), but architected for Europe and beyond, with GeoIP-driven locale switching for French Europe (fr-eu) and English fallback elsewhere.
What “solo” actually means
I built the entire app myself: design, mobile, backend, web, infrastructure, App Store submission. The stack was deliberate.
Mobile. React Native + Expo SDK 54 + Expo Router v6, TypeScript. EAS Build for the native binaries (Android AAB and iOS IPA), EAS Update for OTA JavaScript updates with separate preview and production channels.
Backend. Node.js + TypeScript + Express + MongoDB (Mongoose). REST API versioned at /v1, deployed behind Nginx + SSL with rate limiting and GeoIP-based country detection. Around twenty Express modules, roughly 1,800 lines of TypeScript on the server side.
Web. Next.js 15 (App Router) + React 19 + TypeScript strict, Tailwind 3.4. Trilingual at compile time: fr.ts is the source of truth, en.ts and fr-eu.ts must satisfy the same Dictionary type, so missing translations break the build instead of shipping silently.
Observability. Mixpanel (EU project) for product analytics, Sentry for crashes, JailMonkey for root/jailbreak detection on mobile, SecureStore for tokens.
Solo on the build does not mean solo on the launch. Two things I could not, and should not, do alone. Pretending otherwise would have shipped a worse product.
The two humans the product depended on
The content
I am not qualified to tell parents when to introduce nuts to a nine-month-old, or whether papaya is appropriate for a six-month-old in our climate. I asked a family member with a nutrition background to own the content: what goes in the database, what is age-appropriate, what gets called a recipe and what gets called education. She agreed.
The 39 recipes and 46 documented foods that ship in v1 are her work, not mine. Extracted from West African pediatric nutrition references, with nutritional values per 100g, recommended portions, and benefits sourced from FAO/ANSES data adapted to our region. The trust users place in the app is the trust she earned by being a qualified contributor. Not the trust I earned by writing the UI. That’s the single most important thing about the product, and I almost forgot to write it down.
The first testers
I needed two cohorts before launch. A few nutritionists who could spot anything dangerous in the content flow. A few real parents willing to install a TestFlight build and report back. Neither group was in my contacts on day one.
Day three I spent a day and a half running outreach. DMing nutritionists I found on LinkedIn, picking ones who already wrote about pediatric nutrition or African food contexts. Pinging WhatsApp groups of parents in my network and a few removed. Following up the same day, because asking once and waiting kills outreach.
Out of around forty messages, eight useful responses came back: three nutritionists willing to look at the content, five parents willing to install the build. That was enough.
This is the part nobody puts in their shipping log because it looks like it doesn’t count. It counts. Without those eight humans, what I am shipping is a TestFlight build, not a product.
Apple, three times
This is the section the previous version of this post did not have, because I had not written it honestly enough. The five-day sprint had three Apple rejections in the middle of it.
Rejection 1, then again: 3.1.1 (in-app purchases)
Apple’s 3.1.1 guideline says digital goods sold inside iOS apps must use in-app purchase. We had launched with our own payment flow for consultations with nutritionists: direct card, Orange Money, Wave. Reasonable from a West African market perspective. Not reasonable from Apple’s.
Rejection. Reviewer attached the offending screen.
I had two real options:
- Spend roughly two weeks integrating StoreKit IAP. Meaningful engineering, made awkward by the fact that the seller in the consultation flow isn’t us, it’s the nutritionist.
- Remove every payment entry point from iOS entirely, keep monetisation on Android and the web, accept that iOS becomes fully free.
I chose option two on the morning of day three. Removed every payment screen, every redirect, every CTA to a paid plan from the iOS build. Kept the consultations feature itself, just with no payment step on iOS. Resubmitted.
Rejection again. 3.1.1, second round. The reviewer had found a redirect in one corner of the subscribe screen I had missed. Removed that, resubmitted, approved on the third pass.
The reason I would defend this trade-off looking back: payment isn’t the activation moment for this product. Activation is seeing a recipe adapted to my child’s age, with my country’s ingredients, in my language. If I had taken two weeks to integrate StoreKit, I would have delayed v1 by two weeks to test that activation hypothesis. Android (the larger market for us by a wide margin) keeps its payment flow. iOS users can subscribe through the web. The product’s monetisation isn’t blocked. The product’s learning loop opens immediately.
Cutting code you have already written, under regulatory pressure, is harder than writing it in the first place. I’d seen the principle stated a hundred times; living it on a deadline was a different exercise.
Rejection 2: 5.1.1(ix) (pediatric health)
This one was not a code problem. Apple flags health apps that provide guidance to children, and one of the requirements is that the publishing Apple Developer account must match the editing entity by legal name. Our editor (Digitall Elevate, a Beninese SA) needed identity verification through a D-U-N-S number from Dun & Bradstreet, and ideally an Apple Organisation account.
Day two went into: file the D-U-N-S application, file the Apple Organisation upgrade, hope they both come back fast.
Both came back faster than I expected. Apple actually validated the entity without requiring the full Organisation conversion once the D-U-N-S (850448331) was attached.
The thing I would tell anyone shipping a health-adjacent app to iOS: do the legal entity work before you start the engineering, not in parallel. The work is procedural, not technical, and it can hold the whole launch hostage in ways no amount of dev velocity will compensate for.
Rejection 3: 2.1 (videos didn’t play)
Smaller story. Day four. The mini-courses video player was a placeholder. The backend had no video URLs seeded yet. Apple’s reviewer noticed. Rejection.
Removed the video player from the build, resubmitted the same day, approved. Brought the player back later via OTA once the backend had real URLs, and once we’d swapped the raw WebView for react-native-youtube-iframe (the WebView version rendered black on Android, another half-day of triage).
Lesson, and one I keep relearning: don’t ship UI for features the backend can’t serve yet, even if you plan to fill it in soon. The reviewer doesn’t care about your roadmap.
The architecture that absorbed all of this
The reason the three rejections didn’t compound, each fix shipping in under a day, is that we had over-invested in adaptability before we knew we’d need it. Three architectural decisions, in hindsight critical.
OTA updates everywhere. EAS Update channels (preview and production) let us patch the JavaScript layer in roughly three minutes without going through the App Store. Every rejection that didn’t change native code was a same-day fix. The 2.1 video removal was 22 minutes from rejection email to resubmission.
Backend-driven content. The mobile app is a thin shell. Recipes, foods, articles, plans, all from the API. No content lives in the bundle. When the content owner adds a new food to the database, it appears in the app on the next launch, no App Store review needed.
CDN-with-emoji-fallback for icons. Each food and recipe has an optional image_url field (WebP 256×256, served from cdn.enutri.app with immutable 1-year cache). When set, the app renders the image. When null, it renders an emoji. v1 ships with 46 of 46 foods and 37 of 39 recipes already populated with real illustrations, but the fallback is the safety net for the cases I couldn’t predict: a new food added before its illustration is produced, a CDN outage, an asset pulled for legal review. In every one of those cases the app degrades to emoji instead of breaking. No release needed.
The pattern across these three: build for the moments you haven’t predicted yet. Regulatory surprises, content delays, vendor failures. The cheap version of every decision locks you in. The slightly more expensive version lets you respond to the world as it actually shows up.
The bug that ate three hours
One concrete story, because real shipping logs need at least one.
Day four. The emoji matcher on the age-range filter, which picks which icon to show for a recipe given a target age range, was returning empty for half our recipes.
The backend was emitting age ranges with en-dashes (6–9 mois) and the matcher in the app was comparing against strings with regular hyphens (6-9 mois). Visually identical. Byte-level different.
Three hours to find. Five-minute fix. The kind of bug that doesn’t show up in any unit test you would have thought to write, and only surfaces because a real reviewer used a real device on a real recipe.
I keep this one on a sticky note. The lesson isn’t “be careful with character encoding.” The lesson is: even after every test passes, you have not seen the bugs your real users will see in their first hour. Ship to real humans early, instrument everything, debug from telemetry, not from imagination.
Measurement before features
The thing I most regret leaving out of the previous version of this article: we did the measurement work before v1 was feature-complete.
Before submission, the instrumentation was already live:
- One defined NSM: parents nourris chaque semaine (parents who plan or cook for their child at least once a week). Not downloads. Not signups. Not DAU. The thing that actually means the product is working.
- Three instrumented funnels: acquisition to activation, weekly engagement, four-week retention.
- Super-properties on every event:
has_account,has_child,country. So we can slice every funnel without re-instrumenting. - One consolidated event,
Nutrition Content Viewed, that fires identically across recipes, foods, articles, and plans. Single source of truth for “the user looked at nutrition content,” regardless of which surface produced the view.
This is the part of the launch I am most proud of and that is most likely invisible from outside. Without it, post-launch decisions are vibes. With it, they are real numbers with a defined unit of value, which is rarer than it sounds.
What is missing in v1
Honest list. Short, because the field constraints were taken seriously up front and most of what people expect to be cut from a five-day launch is actually in v1.
- iOS monetisation path. Currently no payment on iOS. Either StoreKit IAP or a web-redirect flow with proper messaging. Decision coming once the first retention cohort lands and the unit economics are clearer.
- Push milestone editorial. The Expo Push pipeline is wired and the milestones themselves (6, 9, 12, 18, 24-month birthdays) trigger correctly. The copy around each message still needs to be written, and tested with a few parents before it goes live to the cohort.
The reason that list is so short: most of what tends to get cut from a five-day launch survived this one. The things that did not get sacrificed for speed:
- Multi-child profiles. Parents in our region commonly raise two or three children in the target age range at the same time. The app uses an
ActiveChildContextthat propagates the selected child to the meal plan, recipe-of-the-day, and age guides. One source of truth, no per-screen duplication. - Community. Discussion threads and replies are live in v1. The fourth onboarding promesse isn’t a coming-soon screen, it’s a real feature, with content moderation hooks.
- Real illustrations. 46 of 46 documented foods and 37 of 39 recipes ship with real WebP illustrations through the CDN. The emoji fallback is there for resilience, not because the assets are missing.
- Connectivity-tolerant defaults. 20-second network timeouts, lazy-loaded course videos, low-resolution recipe images on slow networks, server errors logged to Mixpanel and Sentry, AuthGate that defers signup until the user wants to save (not when they arrive). The whole app is designed for poor connections and entry-level phones from day one.
- Operational dashboards. Mixpanel boards for the NSM, the three funnels, and the country slices are live before the App Store approval landed. Post-launch decisions get to start with data on day one, not month two.
What this taught me
Four things, in descending order of importance.
1. Solo shipping is two-thirds people work, one-third code. I spent roughly two days writing software and three days on humans, paperwork, and one painful pivot. The human side was harder. It is also the side that determines whether the launch produces a product or just a build.
2. The strategic call is the one regulatory pressure forces you into. I didn’t choose to make iOS free. Apple 3.1.1 chose for me. The senior PM move was to look at the constraint and ask “what’s the smallest version of this constraint I can accept, and what does it cost me?”, not to fight a guideline I was going to lose against. Cutting StoreKit work and accepting iOS-as-free was uncomfortable. It was also obviously right within about ninety minutes of doing the math.
3. Build for the surprises you haven’t predicted yet. OTA, backend-driven content, CDN fallbacks. None of these were strictly necessary for v1. All three were what made the three Apple rejections recoverable inside a five-day window instead of a five-week one. Adaptability is engineering insurance, and the premium is cheaper than people think.
4. Measurement before features. NSM, funnels, super-properties, consolidated events. All live before the product was. Anything else and post-launch is decided by whoever shouts loudest. With it, post-launch is decided by whoever can read the curve.
Try Enutri