We will be undergoing planned maintenance on Oct 7th 6:00AM UTC / Oct 7th 2:00AM ET

Inspiration

I'm a heavy reader, and I got tired of a very specific, very avoidable problem: the moment I want to actually picture a character I'm reading about, my options are either (a) do nothing and let my imagination run on fumes, or (b) search their name online — where fan art, casting-call posts, and Pinterest boards routinely spoil plot points that haven't happened yet in my book. Wanting a face for a character shouldn't cost you the ending.

BookLore AI is my answer to that: type in the book, the character, and — optionally — a quote, and get back an original AI portrait grounded in what's actually true about that character at that point in the story, generated in a couple of seconds, with nothing pulled from the wider internet's spoiler-soup.

What it does

BookLore AI is a native SwiftUI app where you:

  • Search for a book (Google Books, with an Open Library fallback for exact ISBN lookups)
  • Pick or type a character, add an optional quote for extra grounding
  • Choose a visual style preset
  • Generate an original character portrait in about 2 seconds, backed by fal.ai's FLUX model
  • Build out a shelf of books and a gallery of characters, share portraits as branded cards, and set a custom cover from your camera roll for books that don't have official cover art
  • SPOILER-SAFE, TRACKED TO YOUR PROGRESS — Mark how far you've read (Just Started / Partway / Nearly Done, or Finished for full accuracy) and every generated portrait and quote only reflects what's true up to that point in the story.

The "no spoilers" promise isn't just marketing copy — it's an actual constraint I had to design the AI pipeline around.

How I built it

Client: 100% native SwiftUI, SwiftData + CloudKit for storage (a library follows your Apple ID across devices, no backend database for user content), RevenueCat for the paywall/subscription/credit-pack layer.

Backend: a small Node/Express proxy on Render. It never lets the client talk to fal.ai, Gemini, or RevenueCat's secret-key endpoints directly — those keys never ship in the binary. It validates input, runs content moderation, checks entitlement/credit balance against RevenueCat's own records (never the client's local state), calls the image model, and refunds on failure.

Generation pipeline, per request:

  1. Gemini looks up the character's real, book-grounded physical description — hair, build, signature item, setting — instructed to omit anything it isn't confident about, and to describe the character as they appear for most of the book, not a late-story transformation or reveal.
  2. That description feeds into a structured prompt alongside the chosen style preset.
  3. fal.ai's FLUX model renders the portrait, typically in ~2 seconds.
  4. The result attaches to the character and can be exported as a shareable card.

Monetization: RevenueCat Virtual Currency for the credit-pack economy (a CREDITS ledger that lives entirely server-side, checked before every generation) plus a Pro subscription for unlimited generations. I built and ran a purchasing-power-parity pricing pass across ~170 App Store territories using real World Bank PPP and exchange-rate data, rather than flat USD pricing everywhere.

Challenges I ran into

Spoilers are a design constraint, not a content filter. The naive version of "describe this character" pulls from a model's full knowledge — including how the book ends, and everything in the sequels. I hit this directly: generating Feyre from A Court of Thorns and Roses came back with fae features that are literally the book's ending reveal. The fix meant explicitly instructing the description step to default to the character's baseline/early appearance and never reach into sequel-only facts.

Hallucination vs. usefulness, in both directions. A text model asked to describe a book character will happily invent plausible-sounding details it doesn't actually know. Clamp it too hard and you get "unknown" for characters it does know well. I settled on a strict binary contract: state only high-confidence details, or say nothing at all — no hedged middle ground.

Real entitlement plumbing, not just a paywall screen. Making "pro" status and credit balance always re-derive from RevenueCat's own records server-side took real care — purchase-in-progress UI that blocks every other tappable control while a purchase/restore runs (visible spinners, not just silent disabling), and a per-attempt idempotency key on the backend's credit-spend call to close a double-spend gap on network retries.

Xcode silently rewriting signing state. A recurring, genuinely surprising one: Xcode's Signing & Capabilities UI repeatedly rewrote the team ID, bundle identifier, and CloudKit container entry in the entitlements files — sometimes only in Release, not Debug. Direct file inspection before every archive became non-negotiable.

A custom launch screen that actually holds up on-device. iOS's real-device launch-screen snapshot pipeline doesn't reliably load custom fonts, even though Simulator's does. Fixed by pre-rendering the wordmark as a static image from the real font file instead of live text, sidestepping font-loading at launch entirely.

What I learned

  • Spoiler-safety has to live in the prompt-engineering layer, because a model's "knowledge" doesn't know where the reader currently is in the story.
  • PPP-adjusted pricing is doable with public World Bank data and App Store Connect's price-point-equalization API — but the API has real sharp edges (batched vs. per-territory submission differs by product type, and the base territory is silently excluded from equalization results).
  • CloudKit's background sync depends on the remote-notification background mode and aps-environment entitlement even with zero explicit push code — a capability that looks "unused" by grep can still be structurally load-bearing.
  • Building the monetization layer to be honestly verifiable (server-authoritative balance, no client-trusted state, idempotent spend/refund) took more care than the paywall UI itself, and was worth it.## Inspiration

Accomplishments that I am proud of

  • Shipping a real spoiler-safety guarantee baked into the AI pipeline itself, not just a marketing claim — a book app that respects exactly where you are in the story.
  • A fully server-authoritative monetization system: credit balance and pro status are never trusted from the client, purchase state is idempotent against network retries, and every purchase/restore path is properly blocked in the UI while in flight — no way to double-spend or double-purchase.
  • Real, individually-computed purchasing-power-parity pricing across ~170 App Store territories, built from actual World Bank economic data rather than a flat one-size-fits-all USD price.
  • A fully native SwiftUI experience with CloudKit sync, a custom brand launch screen, shareable character cards, and a barcode/camera-roll book-adding flow, all built and hardened solo within the Shipaton timeline.
  • Getting all the way through a full App Store Review Guidelines self-audit and successfully submitting for review.
  • Re-engagement done right — a server-side job nudges genuinely inactive users (3+ days, real credit balance or active subscription) via direct APNs, not a third-party SDK, capped so nobody gets spammed daily.

What's next for BookLore AI

  • More generation control: style variations per character, re-roll without a full credit, richer pose/scene options.
  • Group/scene generation: multiple characters rendered together in one scene.
  • Android, once the iOS + RevenueCat foundation proves out post-launch.
  • Deeper CloudKit-based social features — sharing a shelf or gallery with friends, staying private-by-default.

Built With

Share this project:

Updates

Submission history