Skip to content
D · 04 · TECHNICAL WRITE-UP

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.

01 · WHAT IT DOES

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-buildingChat 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 toggleEvery 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 generationPick 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 generationImagen 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 readerStory renderer styled like a story book (Fredoka + Nunito, parchment background). Print mode generates separate parent and child booklet layouts via window.print().
  • Parent lockPIN-gates settings, image regeneration, and any cost-sensitive surfaces. The grown-ups stay in control.
  • Story archive + variation trackerEvery generated story is saved. Past titles and synopses feed back into the prompt so the next story doesn't repeat itself.
  • Cloud syncFirestore-backed sync for worlds, characters, and saved stories. Auto-sync runs in the background so a story started on one device finishes on another.
02 · NOTABLE DESIGN CHOICES

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.
03 · STACK

What runs underneath.

LayerDetail
FrameworkNext.js 16.1.6 (App Router, React 19, server actions for the 14 API routes).
LanguageTypeScript 5 (strict mode).
StylingTailwind CSS v4 via @tailwindcss/postcss. Dark theme with CSS variables in globals.css. Fredoka (display) + Nunito (body).
StateZustand v5 with persist middleware → localStorage (key: world-smid-store).
AIGoogle Gemini 3.1 Pro Preview via @google/genai for text + structured output. Imagen 4 for every image surface.
StorageFirestore for worlds + archive, GCS for generated images.
RuntimeGoogle Cloud Run europe-west4. Source deploy — no Dockerfile, no cloudbuild.yaml; gcloud run deploy auto-containerises the Next.js app.
WHY THIS EXISTS

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.

Read the source on GitHub →