Case study · 2026
Prajna
An AI storybook app for kids. I built the whole platform: the generation engine, the backend, the Android app and the admin panel.
- Role
- Founding AI Engineer, XXAVI AI Tech
- Scope
- Sole engineer: backend, Android app, admin panel
- Platform
- Android app, web admin panel
- AI cost per illustrated story
- $0.48 → $0.11
- cheaper retries after a failure
- 86%
- AI gateway overhead per call
- < 70 ms
- for a 7-image illustrated story
- ~74 s
01 — Overview
Prajna is an AI storybook and creativity app for children aged 3 to 12. It has illustrated picture books in several languages, read aloud, and Create, where a child's idea or drawing becomes a finished picture.
02 — The problem
AI image generation is slow, expensive and fails often. One story is 11 steps and 7 paid image calls, and deploys restart the worker, so interruptions are routine, not rare.
Families pay in credits, so a lost job is lost money. Everything a child sees has to be safe, and a child's data has to stay private, including in the logs.
03 — Architecture
How one generation travels, from the tap to the finished story.
- 01
API
Saves the job and takes the credits in one transaction. Sends nothing.
- 02
Postgres outbox
The job row is the outbox: written, not yet on any queue.
- 03
Dispatcher
Every second, sends unsent jobs to the queue, then stamps them.
- 04
SQS
At-least-once delivery, with a dead-letter queue behind it.
- 05
Worker + engine
Runs the pipeline step by step and saves each step before the next.
- 06
AI gateway
OpenRouter, then Gemini, then OpenAI, with circuit breakers.
Reconciler. Runs every minute beside them. It finds jobs that went quiet (worker killed, never dispatched) and marks them abandoned, which triggers the refund.
04 — Engineering decisions
- 01
A generation engine that survives crashes
Every step saves its output before the next one starts, so a crash or deploy resumes at the first unfinished step instead of starting again. A unique index on (job, step) makes duplicate queue deliveries safe: the worker that loses the race reuses the winner's result.
A failure on image 6 of 7 re-runs 1 image ($0.016), not the whole story ($0.11). Retries are 86% cheaper.
- 02
No job lost between the database and the queue
Saving a job to Postgres and sending it to SQS can't happen in one transaction. So the API only commits, a dispatcher is the only thing that sends (a transactional outbox), and a reconciler detects stuck jobs from the outside, because the code that would report a crash is the code that didn't run.
Every failure path ends in an automatic refund. Zero lost or double-billed jobs.
- 03
Multi-stage GenAI pipelines on one engine
Prompt enhancement, LLM story writing, a character reference sheet, scene generation, an LLM judge that checks each scene against the sheet, then PDF assembly. A pipeline is a list of steps, so 9 of them share one engine, including LLM translation and narration across 3 voice providers.
A 7-image illustrated story in about 74 seconds, with the same characters on every page.
- 04
One AI gateway, three providers
Every model call goes through one place. Circuit breakers are scoped per provider and per task, so an image outage can't trip text work. Permanent errors fail fast instead of burning retries, and provider error text, which echoes the prompt describing a child, never leaves the gateway. Admins swap models per feature with no deploy, and a daily spend cap stops generation before a runaway bill.
Less than 70 ms added per call.
- 05
Measure the cost, then cut it
The cost of every AI call is recorded per step. Images turned out to be 96% of the bill, so that is where the work went: benchmarking image models on real stories instead of price lists. The judge that checks each picture costs 11% of the picture it checks.
$0.48 → $0.11 per story, 77% lower.
- 06
Money that stays correct under load
Credits are debited in one conditional UPDATE, never read then write. A refund can only happen once because a unique index enforces it, not because the code is careful. I audited every path that moves money and fixed 4 billing bugs, including a webhook deadlock between two events for the same family, fixed with one lock order everywhere.
The deadlock reproduced in 29 of 30 runs. Now: 10 concurrent spends on a balance of 20, exactly 4 succeed.
- 07
Children's data stays out of the logs
Redacting by field name missed the real leak: Postgres and ORM error messages carry the failing row, names and birth dates included, inside a string. I replaced it with a recursive scrubber that also strips query values from messages and stack traces. Children's photos upload straight to storage through presigned URLs, never touch the API, and are deleted within 24 hours.
90× faster than pino's built-in redaction: 12 ms vs 1,112 ms per 20,000 log lines.
05 — Also built
Android app
React Native and Expo. A typed API client generated from the backend contract, so an API change breaks the build, not production. Offline cache, in-app purchases, push notifications with deep links, and a sideways-book reader with narration.
Admin panel
React. A draft, review, approved, published workflow for every book, the AI spend dashboard and cap, failed-job and moderation queues, model routing per feature, and an audit log.
06 — Stack
- Backend
- TypeScript, Node.js, Express, oRPC, Zod, Drizzle
- Data
- PostgreSQL, Valkey, SQS + DLQ
- AI
- OpenRouter, Gemini, OpenAI, Sarvam, Gnani
- AWS
- EC2, RDS, ElastiCache, S3 + CloudFront, SES
- App
- React Native, Expo, TanStack Query, RevenueCat
- Ops
- Sentry, pino, systemd