Go Deeper
I build this with Claude Code on GCP after a suggestion by my 8 year old. It has some great features like custom character generation, history, favorites, voice options and a easy to navigate UI.
Stack Used
Created with Claude Code, trial and error
Framework - React 19 with Build tool
Vite 7 (fast HMR, Rollup-based prod builds PWA - vite-plugin-pwa — service worker, offline caching, installable on mobile
Routing - React Router v7 (SPA with /, /story, /settings, /history routes)
State - Zustand with persist middleware → localStorage (playback mode, voice, age range, story, management, history, favorites)
Styling - CSS Modules (.module.css per component, scoped class names)
Auth - Firebase Anonymous Auth (auto sign-in, no user credentials needed)
Data - Firestore SDK — addDoc to write requests, onSnapshot for real-time responses
Backend (GCP / Firebase)
Hosting - Firebase Hosting (CDN-backed static hosting)
Functions - Firebase Cloud Functions v1, Node.js 20, Firestore-triggered (onCreate)
AI / Story - Vertex AI Gemini 2.0 Flash via @google-cloud/vertexai SDK, generation
Text-to-Speech - Google Cloud TTS (multiple natural voices — Journey, Neural2, WaveNet families)
Database - Cloud Firestore — storyRequests and ttsRequests collections as async job queues
Frontend writes a "pending" doc → Firestore trigger fires Cloud Function → function updates
Architecture - doc to "complete" → frontend onSnapshot picks up the result. This avoids HTTP pattern entirely, which is required due to a GCP org policy (iam.allowedPolicyMemberDomains) blocks allUsers IAM bindings.
Key Design Decisions
- No HTTP Cloud Functions — org policy forces the Firestore-trigger pattern for all backend calls
- Retry with cold-start tolerance — 45s first timeout + 30s automatic retry for story generation
- Dynamic fallback stories — if Gemini fails after retries, a template-based story is served so kids are never left waiting
- JSON repair — if Gemini returns malformed JSON, a second Gemini call attempts to fix it before falling back
- RAF-driven timer — requestAnimationFrame loop with ~30 FPS throttling instead of setInterval, avoids drift and keeps the SVG ring smooth
- iOS audio — silent MP3 unlock on user gesture to satisfy WebKit autoplay restrictions