Bedtime Stories.
A Dutch AI bedtime-story generator. Creature Team Verhalenmachine.
Build a fantasy world with characters. Configure a story. Get a multi-chapter illustrated bedtime story, complete with a parent reading aid, quizzes, hero portraits, coloring pages, and a parchment reader that prints as separate parent and child booklets. Dutch in, Dutch out. For one specific family first; published as a portfolio version so the engineering work is legible.
Eight capabilities, one bedtime.
The app is built around the Creature Team universe — a pre-loaded cast of around fifty heroes, sidekicks, and villains the kids can mix and match. Everything else is configuration on top of that.
- Conversational world-building — Chat with the world editor (Gemini structured output) to add characters, traits, places, items. The world log updates in place — kids design the universe before the story starts.
- Three-state character toggle — Every character is Mee (forced in), Thuis (forced out), or Willekeurig (random). Story cast is hard-capped at 7 characters — beyond that, narrative coherence collapses.
- Story generation — Pick chapter count, target age, narrative arc, and learning goals. Outline first, then chapters generated in parallel batches of 3 with per-chapter retry. Each chapter ships with a self-read section, a 500–600 word parent-aid section, 5 quiz challenges, and an image prompt.
- Image generation — Imagen 4 handles covers, in-chapter illustrations, coloring pages, character profile pictures, and per-pose character sheets. Generated assets are persisted to GCS and recovered on demand.
- Parchment reader — Story renderer styled like a story book (Fredoka + Nunito, parchment background). Print mode generates separate parent and child booklet layouts via window.print().
- Parent lock — PIN-gates settings, image regeneration, and any cost-sensitive surfaces. The grown-ups stay in control.
- Story archive + variation tracker — Every generated story is saved. Past titles and synopses feed back into the prompt so the next story doesn't repeat itself.
- Cloud sync — Firestore-backed sync for worlds, characters, and saved stories. Auto-sync runs in the background so a story started on one device finishes on another.
The invariants worth knowing.
These are the non-obvious rules the codebase depends on — the kind of detail that costs a weekend to learn the hard way. Surfaced here so the next maintainer (future me) doesn't.
- Outline first, then chapters in parallel
- The pipeline generates an outline, then fans out three chapters at a time with per-chapter retry. Promise.allSettled keeps a single failing chapter from killing the run.
- Story cast is hard-capped at 7
- Beyond seven, the model starts losing track of who is in the room. The cap was enforced after early stories with larger casts kept producing characters who appeared once and vanished.
- Never pretty-print the world log into a prompt
- The world holds ~50 characters with long descriptions. JSON.stringify with indentation blows the token budget. Compact helpers in store.ts (buildWorldSummary, plus 200-char truncation in story prompts, 150-char in chat prompts) are mandatory.
- Outline is passed as one-line-per-chapter into the chapter prompts
- Same token-budget reason. The chapter generator sees the outline as a list of one-liners, not the structured JSON, so each per-chapter call stays focused.
- Dutch in, Dutch out — except imagePrompt
- All UI text and generation prompts are in Dutch (the kids are the audience). The single English string in the pipeline is the imagePrompt — Imagen 4 performs measurably better on English visual prompts than on Dutch ones.
- Model name is locked
- gemini-3.1-pro-preview is the only AI model wired in. Switching it requires re-verifying every prompt and structured-output schema still parses. Not a casual bump.
- Client-only — no SSR
- Components are 'use client'. The Zustand store relies on localStorage, which doesn't exist server-side. Persistence: localStorage for the active session, Firestore for the cross-device archive.
What runs underneath.
| Layer | Detail |
|---|---|
| Framework | Next.js 16.1.6 (App Router, React 19, server actions for the 14 API routes). |
| Language | TypeScript 5 (strict mode). |
| Styling | Tailwind CSS v4 via @tailwindcss/postcss. Dark theme with CSS variables in globals.css. Fredoka (display) + Nunito (body). |
| State | Zustand v5 with persist middleware → localStorage (key: world-smid-store). |
| AI | Google Gemini 3.1 Pro Preview via @google/genai for text + structured output. Imagen 4 for every image surface. |
| Storage | Firestore for worlds + archive, GCS for generated images. |
| Runtime | Google Cloud Run europe-west4. Source deploy — no Dockerfile, no cloudbuild.yaml; gcloud run deploy auto-containerises the Next.js app. |
The honest version: kids ask for the same kinds of stories with the same characters again and again, and reading every one out loud at 9pm is a real time and energy expense. Bedtime Stories absorbs the part of that loop that is mechanical — the chapter generation, the illustration, the variation tracking — and leaves the choosing to the family. The published portfolio version is the same engine that runs the private family deployment; the private one carries the family's own characters and worlds, which stay on the homelab. That split is only possible because the story engine knows nothing about the worlds it renders — the same discipline Luminary is built on, here letting one engine carry two completely different worlds.