Inspiration
The product thesis is narrower than "bedtime stories for kids". Plenty of apps read to children. None of them sound like the person the child actually wants.
A child who will not settle for a stranger's voice will settle for their parent's — and the parents who most need that are the ones who cannot be in the room: working late, working away, deployed, or on another continent entirely. A large share of them are migrant and diaspora families, where a second pain stacks on the first. A story in Mum's voice, in the heritage language, does two jobs at once.
So the wedge was never "make a story app". It was: can the emotional payload of a parent's own voice be delivered in ten seconds, at a price a parent will pay, without asking them to record for ninety seconds before they get anything.
What it does
The parent reads one short passage aloud — about twenty seconds, once. After that, every bedtime story is generated for their child by name and narrated in the parent's own cloned voice.
Design decisions that shaped everything else:
- Nothing to tap during a story. No games, no questions, no choices. A story that asks a four-year-old something is a story keeping them awake.
- It winds down on purpose. Each page is quieter and slower than the last, and a progressive dim tracks story position, so the screen is darkest exactly when the child is most likely to drift off.
- Screen-off is first class, not a fallback. Phone face down, voice only.
- Replays are free and work offline. Audio is cached on device, keyed by story and voice — which matters because the child is disproportionately at a grandparent's house or on a plane.
- The voice ask comes AFTER the first story, never before. Enrolment before first value is a ninety-second wall in front of someone who has received nothing yet.
The closing line is the product's whole point: "Goodnight, Amir. Daddy loves you." — the one sentence spoken as the parent, in the parent's voice.
How we built it
Client — React Native on Expo (SDK 57), expo-router, Reanimated for the Ken Burns drift and cross-fades, expo-audio for a custom per-page playback queue.
Server — Hono on Cloud Run with min-instances=1, deliberately not Cloud
Functions: a cold start consumes the entire 300ms page-one budget. Firestore for
an append-only credit ledger; Firebase Auth for silent anonymous sessions.
AI — Gemini Flash-Lite for story text, Cartesia for voice cloning and narration, with MiniMax wired as a degradation path. Everything sits behind a provider abstraction written before any vendor SDK was imported, which is what later let us swap the entire narration vendor by changing one line, with no caller aware of it.
Two decisions did most of the work:
Page 1 never touches an LLM. It is pure template interpolation, so the first audio arrives in hundreds of milliseconds rather than seconds. Personalisation starts at page 2, by which point audio is already playing and nobody is waiting.
Generation is lazy, at a prefetch depth of two. Most bedtime stories are never finished — that is the product working, not a failure — so pages past where the child fell asleep are never generated and never billed. The server refuses requests beyond the window rather than trusting the client to stay honest. It is a cost control disguised as a latency feature.
Story safety runs in two independent layers: a constrained-generation system prompt with absolute thematic boundaries, and a separate classifier pass over every generated page before it reaches text-to-speech. The classifier sees the text alone — no prompt, no context — so it cannot inherit the generator's blind spot, and it fails closed to a hand-written, pre-reviewed page. A rejection costs personalisation, never the story.
Challenges we ran into
A grammar bug that only exists in some languages. The story template had slots for the child's name, the theme, the companion and the setting — and none for who is speaking. English hides this: "I love you" works regardless. Malay does not. A parent addressing a small child uses a kinship term for themselves — Ibu sayang kamu ("Mother loves you") or Ayah sayang kamu. There is no neutral form. We found it when a test produced a father's cloned voice saying "Mother loves you". Same problem in Tagalog, Vietnamese, Korean, Japanese, Thai and child-register Mandarin. It breaks precisely at the emotional peak, and precisely in the segment we cared most about. The fix was capturing the speaker's role one tap before recording, and authoring reveal lines per language rather than translating them — a translation would either drop the speaker's identity or invent one.
A feature that had never once worked in production. Voice enrolment failed
for every user from launch. The cause was a missing Firestore composite index on
the abuse rate-limiter's query: it threw FAILED_PRECONDITION, and the
rate-limiter took down the exact feature it existed to protect. The tell was in
the request logs — a 67ms failure, far too fast to be a vendor call. A safety
control that fails closed on its own infrastructure is worse than no control.
Audio that would not stop. Closing a story kept it narrating, and opening a
second played both at once. Three separate causes stacked: remove() frees the
player but never promises to halt playback; a download resolving after teardown
silently recreated a player; and the exit button relied on unmount while the
stack animation kept the screen alive through the transition. Only fixing all
three made it stop.
Three App Store rejections, each teaching something different. Rejected for sharing data with third-party AI services without naming the recipients in the app — the privacy policy alone is explicitly not sufficient. Rejected again for an in-app promo code field, because unlocking paid digital content through any mechanism other than In-App Purchase is a parallel entitlement system regardless of intent. The privacy rejection produced the best change in the codebase: we now send generated pages with a placeholder instead of the child's name and substitute it only after generation and classification, so the AI vendor never receives a child's name at all. The best way to survive "what data do you share" is to share less.
A bug that would have quietly destroyed money. The Firebase JS SDK defaults to in-memory persistence on React Native, so every app launch minted a brand-new anonymous user. That uid keys the credit ledger — so every restart silently destroyed purchased credits, which no app store will restore. Worse, a fresh welcome grant made the balance look plausible. Fixing persistence alone was not enough: session restore is asynchronous, so the sign-in path raced it and often minted a new user anyway. Whether a parent kept what they paid for came down to disk speed.
Accomplishments that we're proud of
It ships, and it is live on the App Store. Not a demo video — a real app with real in-app purchases, a real privacy label, a parental gate and a COPPA posture that holds up.
The unit economics are measured, not modelled. A complete story costs $0.247 to produce — $0.111 of that narration. We know that because we instrumented it rather than estimating, and it is what justifies consumable credit packs over a subscription: a nightly reader would cost $90/year to serve, which no subscription price a parent would accept can cover.
Child data barely leaves the device. First name and an age band, nothing else. The age band never leaves at all. The AI that writes the story never learns the child's name. No ads, no tracking, no child accounts, no analytics SDK.
The abstractions earned their keep. When one narration vendor ran out of credit mid-launch, switching the entire free tier to another was a one-line change — because no route handler had ever been allowed to know a vendor existed.
What we learned
Latency is a product decision, not an optimisation. The rule that page one may never depend on an LLM shaped the data model, the caching, the server topology and the pricing. Every architectural choice traces back to a tired parent who will not wait ten seconds.
Fail closed, but be careful what you close. Two safety controls in this codebase failed closed. The classifier degrading to an authored page was correct. The rate-limiter throwing on a missing index was catastrophic. The difference is whether the fallback is a worse product or no product.
"It works on my device" hides the interesting bugs. Enrolment was broken for every user while working perfectly for us, because we were already enrolled and never hit the free path. The narration outage had the same shape. The paths a first-time user takes are the ones a developer stops exercising on day two.
App Review is a design critique with a slow feedback loop. Every rejection pointed at something genuinely underspecified — not naming data recipients in the app, and building an entitlement mechanism that duplicated the platform's. The fixes made the product better, not merely compliant.
What's next for Snugglee
Android. The client is built and Google Sign-In is wired; Play release is next.
The heritage language, properly. Reveal lines are already authored in twelve languages with the speaker's role handled correctly. The story bodies are still English. Closing that gap is the whole diaspora thesis — pre-rendered art is language-neutral, so the expensive asset amortises across every language added. It is gated on judging cross-lingual clone quality by ear, and on assessing safety-classifier quality per language before enabling it, not after.
Lullabies in the parent's voice, which is the same emotional payload with a harder technical problem: sung voice conversion degrades across vocal ranges.
Better first impressions. More story skeletons, more art, and a shorter enrolment script if cross-lingual quality holds at a shorter sample.
Built With
- android
- cartesia
- expo-audio
- expo-router
- expo.io
- firebase
- firebase-auth
- firestore
- gemini
- google-cloud-run
- hono
- ios
- minimax
- next.js
- node.js
- react-native
- react-native-reanimated
- revenuecat
- storekit
- typescript
Log in or sign up for Devpost to join the conversation.