Skip to Content
📚 MyStoryFlow Docs — Your guide to preserving family stories
Current State (Sep 2026)Book Creation → PDF

Book Creation → PDF

Final verification, 2026-09-06 evening. ✅ Verified by a real run: a brand-new user created a template book, wrote chapter 1, recorded chapter 2, reordered, set a cover, and downloaded a 6×9 PDF with a full-bleed cover, title page, contents with correct page numbers, and chapters in the chosen order. Late fixes found by that run and landed the same evening: writing now clears the placeholder flag so the PDF button enables (1377f6c); the chapter list no longer 500s and numbers from 1 (6ee5176); a recording made for a chapter now fills that chapter instead of appending a new one, and the record page shows only that chapter’s prompts (0bb3385); chapter names show on a phone (7d3758c); cover text auto-contrasts on dark backgrounds (dee04d2). Still open: the cover editor overflows at phone width; the story editor’s toolbar and a setParagraph error after a heading (editor internals, out of scope by the owner’s rule); the conversion quality gate can take about a minute to answer.

Mechanisms updated 2026-09-06 after the fixes landed; see the final verification note below the title.

1. The journey, step by step

#StepScreenAPITablesStatus
1Choose who the book is for, then a template/books/new — two screens (AboutBookStep, PickTemplateStep)GET /api/templates/booksbook_templates✅ 5 curated cards + “Something else” (blank book); title auto-fills from the name
2Create book from a templatewizard “Create my book”POST /api/booksbooks✅ fixed 2026-09-06 (d364c25) — was a 500 on every attempt
3Create book with “Something else” (no template)wizard “Create my book”POST /api/booksbooks✅ verified
4Scaffold placeholder chapters from the templateserver-side in POST /api/books (app/api/books/route.ts:379-429)stories, book_chapters✅ reachable again now that step 2 succeeds; one placeholder stories row per template chapter, linked through addStoryToBook
5Book landing / next action/books/[id]GET /api/books/[id]/storiesbooks, book_chapters, stories✅ verified — “Start with: X” / “Keep going: X” card, chapter list with Not started / In progress / Done
6Add a chapter (“Write it” / “Record it”)/books/[id]/stories/new, /books/[id]/recordPOST /api/books/[id]/storiesstories, book_chapters, books✅ verified
7Write content/books/[id]/stories/[sid]/edit (Tiptap)autosavestories.content, word_count✅ verified (out of scope for this audit otherwise)
8Link an existing story to a bookContentLinkingModalPOST /api/books/[id]/stories/linkbook_chapters✅ fixed 2026-09-06 (d364c25) — positions are 1-based everywhere, the modal converts at its own boundary
9Add an existing story from the story library, a conversation, or a recording/stories, /ai-conversations/[id], /voice-recording/[id]shared AddToBookDialogPOST /api/books/[id]/stories/linkbook_chapters✅ fixed 2026-09-06 (d364c25) — new app/components/books/AddToBookDialog.tsx used by all three screens
10Unlink a chapter/books/[id]/chaptersDELETE .../stories/linkbook_chapters✅ verified
11Reorder chapters (drag & drop)/books/[id]/chaptersPATCH .../stories/reorderbook_chapters.position✅ fixed 2026-09-06 (d364c25) — was always a 400
12Design a cover manually/books/[id]/coverPATCH /api/books/[id]books.cover_design_data✅ fixed 2026-09-06 (293f7d2) — now reaches the PDF
13Generate an AI cover/books/[id]/coverPOST /api/cover/generate-imagebooks.cover_image_url⬜ not verified this round (bills DALL·E 3; not executed)
14Export gating when nothing is finished/books/[id]POST /api/books/[id]/export✅ 422 no_completed_stories
15Export PDF/books/[id] “Make my book (PDF)”POST /api/books/[id]/exportexport_jobs✅ 201, real 9-page PDF, sanitized body, cover as page 1
16PDF body contentlib/pdf/story-content.ts, lib/pdf/html-sanitizer.ts✅ fixed 2026-09-06 (293f7d2) — was raw HTML tags rendered as text
17Cover page in the PDFlib/pdf/cover-design.tsbooks.cover_design_data / cover_image_url✅ fixed 2026-09-06 (293f7d2, 8b11d7d) — full-bleed, no folio
18List past exports/exportGET /api/books/[id]/exportexport_jobs✅ verified
19Download the PDF/export or book pageGET .../export/[jobId]/downloadexport_jobs✅ verified, authenticated proxy

The one amber node left: export is still a synchronous request inside POST /api/books/[id]/export (§5).

2. What was actually tested, and how

The original audit used approach (b) + (c): a real browser driving the running app, with a throwaway user against the live database (a stubbed e2e spec was rejected as evidence — see the retained note below). The same real-session method was re-run after the 2026-09-06 fixes landed, exercising exactly the paths that had been broken:

  • Template creation — created a book from a curated template; POST /api/books returned 201 and the book page showed all of the template’s chapters as “Start This Story” cards immediately (the placeholder-scaffolding loop in app/api/books/route.ts:379-429 ran to completion for the first time).
  • Reorder — a 3-chapter rotation via PATCH .../stories/reorder succeeded and persisted in the stated order; the RPC path (reorder_book_chapters) was hit, confirming migration 202609060002 is applied and reachable.
  • LinkingContentLinkingModal and the new AddToBookDialog both link an existing story at a real 1-based position; addStoryToBook’s response matches the position actually read back from the junction.
  • Export — a book with 3 written chapters and a manually designed cover produced a real 9-page PDF: page 1 is the full-bleed rendered cover (no folio), followed by a title page, a Contents page with real page numbers, one opening page per chapter with “Chapter N” + title, the chapter bodies as clean paragraphs (no visible HTML tags), and a closing colophon page.
  • Unit testsnpx jest __tests__/api/books __tests__/lib/books __tests__/lib/pdf — 53 book/story tests and 88 PDF tests pass. tests/unit/helpers/fake-supabase.ts now enforces the real (book_id, position) unique constraint, so the reorder regression class is caught in CI rather than only in production.

Why the existing e2e specs are still not evidence

Unchanged from the prior audit: tests/e2e/critical/{book-creation-flow,book-cover-design,export-pdf}.spec.ts stub the network (injectFakeSession, stubBooksApi) and assert only that the page renders something. They would still pass against a book system that could not create a book. apps/web-app/.env.test remains a placeholder template, so these specs never authenticate for real. Replacing them is still recommended (§8).

3. What works

Verified against the live database and a real browser session:

  • The wizard renders real data and creates a book from either a curated template or a blank book, with placeholder chapters scaffolded for the template path.
  • Membership writes are correct and consistent for every write path (create, link, unlink, reorder): book_chapters holds the right position, stories.book_id is set, books.story_ids mirrors it, and books.word_count / completed_stories / completion_percentage are recomputed — all in sync, per lib/books/book-membership.ts.
  • Positions are 1-based end to end. The contract is stated once, at the top of book-membership.ts, and every caller (ContentLinkingModal, AddToBookDialog, the reorder UI) converts at its own boundary rather than sending a 0-based index to the API.
  • Reorder is atomic. PATCH .../stories/reorder and any positional insert now go through reorder_book_chapters(uuid, uuid[]) (migration 202609060002) in one transaction; if that RPC is ever unavailable, writePositions falls back to a collision-free two-phase renumber (park on negative positions, then write 1..n) instead of updating one row at a time.
  • Existing stories can join a book from three places: the story library (app/stories/page.tsx, “Add to a book”), an AI conversation (app/ai-conversations/[id]/page.tsx), and a recording (app/voice-recording/[id]/page.tsx) — all through the same shared AddToBookDialog.tsx and the same addStoryToBook write path.
  • /stories/new stories join their book. The page now POSTs to /api/stories, so the junction row is created instead of only setting stories.book_id.
  • The Tiptap editor autosaves (unchanged, out of scope).
  • Export gating works. A book with no finished stories returns 422 {"code":"no_completed_stories"}.
  • Export produces a real, readable PDF. Stories are rendered by actual format — HTML through an allow-list sanitizer, TipTap JSON, or plain text — not force-parsed as one format and escaped on failure. Chapter openings show “Chapter N” and the title; a story’s own leading heading is stripped only when it repeats that title. The Contents page carries real page numbers from a two-pass render. Fonts (PT Serif body, Playfair Display display) are embedded from @fontsource files read off disk at runtime, with outputFileTracingIncludes in next.config.ts so Vercel ships them instead of silently falling back to no embedded fonts.
  • A saved cover design reaches the PDF. books.cover_design_data is rebuilt server-side as a full-bleed page 1 (precedence: saved design → cover_image_url → typographic fallback), rendered a second time without Chromium’s running folio and spliced over page 1 with pdf-lib — the rest of the document keeps its normal pagination and folios.
  • /export and the download proxy are unchanged and real: export_jobs rows with page count, size, status, and an authenticated download stream.

4. What changed on 2026-09-06

  • Template creation no longer 500s. track_template_usage() called increment_template_usage(NEW.template_id) with a character varying argument against a function that only accepted uuid; Postgres has no implicit cast for that. Migration 202609060001_fix_template_usage_trigger adds a text overload and rewrites the trigger so a usage-counter failure can never fail the book insert (it’s now caught and downgraded to a WARNING). Fixed in d364c25.
  • Chapter reorder no longer collides with the (book_id, position) unique constraint. All renumbering — reorder and positional inserts alike — now goes through reorder_book_chapters(uuid, uuid[]) (migration 202609060002_reorder_book_chapters), run atomically in one transaction, with a two-phase client-side fallback if the RPC isn’t deployed. Fixed in d364c25.
  • Positional linking reports the real position. addStoryToBook now returns the position actually read back from the junction after the write, not the one requested. Fixed in d364c25.
  • ContentLinkingModal’s off-by-one is gone. The position contract (1-based everywhere) is documented once at the top of book-membership.ts, and the modal converts its own 0-based chapter index at its own boundary before calling the API. Fixed in d364c25.
  • Existing stories have a working way into a book. New shared app/components/books/AddToBookDialog.tsx ({ storyId, open, onOpenChange, onAdded? }), wired into the story library, the AI-conversation detail page, and the recording detail page. Fixed in d364c25.
  • /stories/new stories are no longer orphaned. The page now POSTs to /api/stories instead of inserting directly with the browser client, so the junction row is created. Fixed in d364c25.
  • The exported PDF no longer prints raw HTML tags. lib/pdf/story-content.ts detects HTML vs. TipTap JSON vs. plain text per story and renders each correctly; HTML goes through an allow-list sanitizer (lib/pdf/html-sanitizer.ts) instead of being escaped as one giant text node. Fixed in 293f7d2.
  • Chapter titles and numbers render in the PDF body. Each chapter opens on a new page with “Chapter N” and the title; a leading heading that only repeats the title is stripped so it doesn’t print twice. Fixed in 293f7d2, 8b11d7d.
  • A manually designed cover reaches the PDF. The saved cover_design_data is rendered server-side as page 1 (precedence: saved design → cover_image_url → typographic fallback), spliced in without a running folio. Fixed in 293f7d2, 8b11d7d.
  • Fonts ship on Vercel. book-fonts.ts reads @fontsource woff2 files off disk at runtime instead of importing them, with a matching outputFileTracingIncludes glob in next.config.ts, so the deployed function carries the fonts instead of degrading silently. Fixed in 8b11d7d.
  • The test double now matches the live schema. fake-supabase.ts enforces the real (book_id, position) unique constraint, so this class of bug fails in Jest, not just in production. Fixed in d364c25.
  • The wizard is two screens, not eight, and creation is free. See §7 — this doesn’t change the mechanism described in §1-3 above, but it is the biggest change to how a user actually gets to step 2.

5. What is still open

  • POST /api/stories still requires a book_id. Verified: {title, content, status}400 {"error":"Title and book_id are required"}. A story cannot be created standalone through this route. Not touched by today’s fixes.
  • Export is still synchronous. POST /api/books/[id]/export generates the PDF inline (app/api/books/[id]/export/route.ts, status: 201 on completion in the same request) with no queue and no maxDuration override. It took a few seconds for 3 short chapters; a full-length book on Vercel still risks the function timeout with no resumability. Not verified on Vercel — this remains a stated risk, not a measured one.
  • The AI cover path (POST /api/cover/generate-image) was not executed this round — it bills DALL·E 3 against the owner’s OPENAI_API_KEY. Its write to books.cover_image_url and the PDF’s fallback precedence for it are confirmed by code inspection only.
  • Export on Vercel with @sparticuz/chromium has still only been exercised via the local playwright-core path, not measured in production.
  • Where the paywall sits going forward is still the owner’s decision (see §7) — it has been removed from first-run, not replaced with a placement elsewhere yet.

6. Gaps a launch user will hit

  • A blank book still starts completely empty — “Something else” gives no chapters, no prompts, no outline. That’s the intended tradeoff for the curated-template path now that it works; a user who deliberately skips templates gets exactly what they chose.
  • Export is synchronous and unmeasured at real-book length on Vercel — see §5.
  • A story cannot be created without a book (§5) — every /api/stories POST needs a book_id, so there’s still no route to a book-less story via that endpoint (the story library and conversation/recording flows route around this via AddToBookDialog, which links after the fact).

7. Senior-friendliness observations

What changed on 2026-09-06 (b05b8e3, 75a4bcc): the wizard, the book page, and the dashboard were rewritten, and the previous list of UX complaints in this section (8-9 decisions before a book exists, a pre-selected $138 print order, a mandatory Stripe paywall before anything is produced, jargon like “KDP Status: Draft” and “kdp-6x9”) no longer applies to any of these three screens — those steps and fields are deleted, not hidden.

Current mechanism:

  • The wizard is two screens. AboutBookStep (who the book is about — “Me” or “Someone else”, a name, and an auto-filled title) and PickTemplateStep (5 curated template cards, or “Something else” for a blank book). “Create my book” is the button that creates the book; the book page’s later PDF action is separately labeled “Make my book (PDF)” so the two actions no longer share ambiguous wording.
  • No payment screen exists in first run. SubscriptionStep, PrintOrderStep, GoalsStep, RemindersStep, ReviewAndStartStep, and the Stripe checkout call were deleted outright — a trial user cannot reach a paywall by creating a book. “Do this later” and Cancel both return to the dashboard from either wizard screen.
  • Draft restore is silent. Reopening /books/new with an in-progress draft resumes it without a “Start Fresh / Continue Draft?” interstitial.
  • The book page shows one next thing. An empty book gets “Add your first story” with Write it / Record it; a book with chapters gets “Start with: X” / “Keep going: X”, a chapter list using the words Not started / In progress / Done (color-coded), and a “Make my book (PDF)” button that states in plain words why it’s disabled until a chapter is Done, then shows “Making your book… about a minute” followed by “Your book is ready” and a Download button. KDP status, trim size, and language controls are removed from the page.
  • The dashboard leads with one next step. Greeting → the single next-step card → “Add a story” (Write it / Record it) → “My book” (n of N chapters written, Open my book) → Family. The fabricated stats, duplicate continue-card, and Elena promotions from the prior audit are gone (ContinueWhereYouLeftOff.tsx, ReEngagementNudge.tsx deleted); the dashboard is roughly 900-1,300px tall depending on state, down from 8,178px.
  • Navigation no longer links to fake screens. /billing, /orders, /publishing/kdp, /family/shared are removed from the nav; the help page is three real how-do-I answers.
  • Signup’s “check your email” panel is now 3 numbered steps plus resend and “I typed the wrong email”; a confirmed new user lands on /books/new rather than being caught by the old onboarding_step='welcome' redirect trap.

Owner decision, stated for the record: the first-run paywall is removed; where a paywall eventually sits (book assembly, export, or elsewhere) is still undecided.

Most of the prior list is done. What’s left:

  1. (M) Make export asynchronous. Queue the job, return 202, and let /export poll — the stranded-job reaper still doesn’t exist (§5 of docs/RELEASE_BLOCKERS.md operations notes). Needed before a full-length book is exported on Vercel with confidence.
  2. (S) Decide where the paywall goes. The owner removed it from first-run; nothing currently gates export or print. Pick a placement (export and print are the natural candidates) before inviting users who might expect to pay for something.
  3. (S) Let a story exist without a book_id, or explicitly document that every story is book-scoped by design — right now POST /api/stories enforces the latter without saying so anywhere in the product.
  4. (L) Replace the stubbed e2e specs with a seeded real-session suite covering create-from-template → write → reorder → cover → export → download, asserting extracted PDF text. The current specs still pass against a hypothetical broken book system because they never leave the stub.
  5. (M) Verify export on Vercel with @sparticuz/chromium at real-book length, and confirm the embedded-fonts outputFileTracingIncludes glob actually ships in a production build, not just locally.

Test data

Test data created during the original 2026-09-06 audit (a throwaway confirmed user, 3 books, 3 stories, 3 book_chapters, 3 export_jobs) was removed in FK-safe order and verified by SELECT returning 0 rows on every table; the generated PDFs were deleted from Backblaze B2. No rows belonging to any other user were read for modification or deleted. Test data from the post-fix re-verification pass was removed the same way.

Not verified

  • The AI cover generation path (POST /api/cover/generate-image) — confirmed by code inspection only, not executed, to avoid billing DALL·E 3.
  • Export on Vercel’s serverless Chromium (@sparticuz/chromium) — only the local playwright-core path has been exercised.
  • The recording and AI-conversation “Add to a book” buttons (AddToBookDialog, wired into app/voice-recording/[id]/page.tsx and app/ai-conversations/[id]/page.tsx) are new as of d364c25 and were not independently clicked through from those two screens in this pass — the mechanism is confirmed by code and by the story-library instance of the same dialog, not by a run from every entry point.