TL;DR — I just shipped The Wanting Mind, a 147,000-word interactive book that reads like Wikipedia + a podcast + a graph. It's free, no signup, all four languages (EN/JA/ZH/TH). The whole thing is a React 19 SPA backed by Supabase, Capacitor for the mobile apps, and a chunk of AI (ElevenLabs for narration, fal.ai for chapter art, Gemini for video, Claude for translation). This post is what worked, what broke, and the stack at the bottom for fellow indie devs.
The pitch (60 seconds)
It's a long-form book about why we're never satisfied — the economics, neuroscience and Buddhist philosophy of taṇhā (literally "thirst"). Reading it cover-to-cover takes ~10 hours. Most people won't do that, so the app does five things to keep you in:
- A podcast version of every chapter, AI-narrated by ElevenLabs (the same voices, locale-aware)
- An interactive 3D knowledge graph of every concept, person, and chapter —
react-three-fiber+force-graph-3d -
Playable simulations — Easterlin paradox, Minsky moments,
r > g, all the charts you can poke at - Four languages — full translations, switchable mid-sentence
- An "essence" view — every chapter compressed to one quote on top of a generated image, shareable as a card
Free forever. The only paid thing is a $9 lifetime "supporter" tier that unlocks a couple of nice-to-haves (offline downloads, custom voices) — basically a tip jar for people who want to thank me.
Read it: kensaur.us/the-wanting-mind
Source: github.com/kensaurus/the-wanting-mind
The stack
Indie-developer-grade, deliberately. Nothing exotic.
| Layer | Choice | Why |
|---|---|---|
| Framework | React 19 + Vite 8 | Fast dev loop, easy SSG-ish prerender for SEO |
| Language | TypeScript 5.9 | The book has 200+ data files; types catch every typo |
| Styling | Tailwind CSS 4 | Single-file design tokens, dark mode via dark:
|
| 3D | react-three-fiber + drei | Knowledge graph + simulation visuals |
| Mobile | Capacitor 8 | Same React build → iOS + Android, one codebase |
| Auth + DB | Supabase | RLS-only, no server code I have to babysit |
| Audio | ElevenLabs | Multilingual TTS that doesn't sound robotic |
| Images | fal.ai (Flux) | Chapter essence images, batch-generated |
| Video | Gemini Veo 3.1 | Trailer + 30-sec teaser per chapter |
| Translation | Claude (Anthropic) | Chapter-by-chapter, with a human review pass |
| Hosting | S3 + CloudFront | $3/month for the entire site at current scale |
| Errors | Sentry | I cannot afford to babysit a hundred users by hand |
| Push | Firebase FCM | Web push for new chapter releases |
| Tracing | Langfuse | Every LLM call has a trace + cost line |
Three things that turned out to matter way more than I expected
1. Pre-rendering chapter HTML for SEO
A 25-chapter SPA gives you one indexed page (the shell) by default. Googlebot doesn't run your React. So I added a tiny script — scripts/prerender-chapter-html.mjs — that uses Puppeteer to navigate to every /{locale}/{chapterId} route, wait for the content to render, and dump the resulting HTML to dist/prerender/. The deploy then uploads each prerendered HTML to the matching S3 key, so Googlebot hitting /en/ch-4 gets the chapter content rendered, in English, with the right <title> and <meta description>.
Writing this took an afternoon. The day after I shipped it, indexed-page count went from 1 to ~150 across the four locales. If you have an SPA and you care about SEO at all, do this before you do any keyword research.
2. Sharable quote cards generated in-browser via Canvas
Every chapter has an "essence" — one sentence that compresses the chapter. I render those as 1080x1080 cards using HTML Canvas: the chapter's essence image as the backdrop, a translucent overlay, the quote in a clean serif, the book title at the bottom.
The function lives in src/components/QuoteCard.tsx as generateQuoteCardBlob. It runs entirely in the browser, no server. Users can right-click → save, or hit a "share" button that uses navigator.share() on mobile. For the launch, I drove this exact same function from a Playwright script to generate 10 hero cards for socials — zero divergence between what users see and what I post on Bluesky.
3. Idempotent, programmable App Store + Play Store updates
Updating store listing copy by hand across en-US, ja-JP, zh-Hans, th-TH for both Apple and Google is a soul-crushing afternoon. So I wrote two scripts:
-
scripts/asc-update-listing.mjs— talks to the App Store Connect API, JWT-signs with a.p8key, parses the canonical listing copy fromdocs/store-listings.md, and pushes the localized name/subtitle/promo/description/keywords for each locale. -
scripts/play-update-listing.mjs— same idea, but Google Play Publisher API with a service account JSON.
Now my "marketing" repo has a single source of truth (docs/store-listings.md) and one command per store updates everything. This was the single highest-leverage tool I built post-launch.
What didn't work
- Tried to inline the entire knowledge graph data into the bundle. Initial JS payload went from 380KB → 2.1MB. Reverted to lazy-loading on graph mount.
- First pass at audio used streaming generation per request. Burned $40 in API costs in two days from Bingbot. Switched to pre-generated MP3s served from Supabase Storage.
- Pretended "I'll add Stripe later." Adding a payment link in week 1 cost me 30 minutes; trying to retrofit a paid tier into an existing user base would have cost weeks.
If you're going to build something similar
- Pick one mobile shell (Capacitor or Tauri). Don't double up. The feature regression cost of maintaining two shells is brutal.
- Pre-render for SEO before you write a single growth post. Otherwise Google doesn't know you exist.
- Wire up Sentry on day 1, even on a personal project. You will not notice the bug your one user hit yesterday until they tell their friends about it.
- Generate marketing assets from your app code, not separately. The branding stays consistent and you can regenerate at will.
- Keep the entire content layer (book chapters, essences, knowledge graph nodes) as plain TypeScript files. No CMS. The file system + git is the best CMS ever made for a single author.
What's next
I'm going to write one essay a week riffing on a chapter — Substack + Medium + cross-posts here. The first one is "Happiness stopped rising in 1972 — here's what happened" (the Easterlin paradox). If that interests you, the book is here and the chapter on it is here.
If you want to see the actual code, the repo is open and PRs are welcome — especially translation fixes from native speakers.
Thanks for reading. If this helped, an emoji reaction is enough. ❤️
Top comments (0)