← Work
Case study · AI

MandarinAI

MandarinAI is a Chinese-learning platform I designed and built on my own. Its core is a closed loop between spaced repetition and reading: an FSRS scheduler decides which words you're about to forget, and an LLM writes graded stories from exactly those words, so every review earns its keep. It runs edge-native — one Cloudflare Worker serving both the app and the API — on TanStack Start, Hono, Drizzle and Postgres.

MandarinAI
Solo
Designed, built, and shipped single-handedly
HSK 1–9
Every band, across the old 2.0 and new 3.0 standards
Story–SRS loop
Stories written from the exact words your deck says are due
One Worker
SSR and the whole API served from a single Cloudflare Worker

Overview

MandarinAI is a platform for adults who have decided to learn Chinese seriously — the people who have finished a beginner app and hit the wall where flashcards stop being enough. It combines a proper spaced-repetition study loop with AI-generated graded reading, native-quality pronunciation, and HSK-aligned word lists, and I designed, built, and shipped the whole thing myself.

The bet the product makes is narrow and deliberate: most apps keep your flashcards and your reading in separate rooms. You drill a character on Monday and then meet it three weeks later in a story that glosses it as though you'd never been introduced. MandarinAI closes that loop — the word you study and the word you read are the same word — and the engineering that makes that true is the part I find most interesting.

Abstract Chinese brush strokes flowing into a spaced-repetition review timeline of flashcards and a pronunciation waveform

The study loop

The foundation is spaced repetition built on FSRS, the modern successor to the algorithm Anki made famous. Every word you learn carries its own memory model — how stable it is, how difficult, how often you've slipped — and each review updates that model and schedules the next one. That's what lets the app spend your time where it actually matters: on the words you're about to forget, not the ones you already know cold.

That scheduling state is not just a study feature — it's the signal the rest of the product is built on. Because the app knows, per user and per word, what's due, what's fragile, and what's mature, it can do things a generic content app can't.

MandarinAI landing page — 'Master Chinese with confidence', with a Today's Word card showing the character 学 (xué), its pinyin and meaning

Stories written from your own deck

The headline feature is graded stories, and the interesting decision is where the words come from. Rather than serving pre-written content, MandarinAI generates a story from the learner's current spaced-repetition state — the exact words the scheduler says are worth reviewing right now.

A small, deliberately boring piece of logic decides that: a selector ranks the learner's deck and hands the top words to the generator. Words that are due for review are candidates, but words that are fragile — reviewed before and slipping — outrank them, because a word you keep forgetting is worth more reading exposure than one that's simply come around again. The one design subtlety worth mentioning is that "fragile" is defined as studied and slipping, never not yet seen, so a pile of brand-new words can't crowd out the ones you're genuinely losing. The selection is fully deterministic, which keeps it testable and means a given deck always produces a stable, explainable choice of words.

The payoff is that reading stops being decorative. Every story is built from words you already half-know, so it lands in the narrow band between "too easy to bother" and "too hard to parse" — and every review you do earns its keep by becoming something you get to read.

Generating the stories

The generation itself runs on Claude, and it's structured generation rather than free text: the model fills a strict, typed shape — title, sentences, each with Chinese, pinyin, and an English gloss — so the client always receives something it can render directly instead of prose that needs parsing. That contract is what makes AI content dependable enough to build product features on top of.

There's more than one mode. A plain reading mode, an interactive mode where the story branches on choices the reader makes, and a fill-blank mode that masks characters for active recall. There's also a judge: when a learner writes or completes something, a second model call grades the response against a rubric and returns a structured verdict with feedback, so the app can give a real assessment instead of a naive string match.

Because generation costs money and latency, it sits behind an entitlements layer: free accounts get a capped number of story generations per week, Pro removes the cap, and the tier checks live in middleware rather than being sprinkled through the routes. The same layer gates the AI-teacher lessons — scenario-driven dialogues generated to a chosen HSK range and length — while keeping already-generated content readable after a subscription lapses.

Pronunciation and reading, cached at the edge

Audio is the other half of a language app, and it's the part most at odds with a serverless runtime. Word and phrase pronunciation is synthesised by a neural TTS model, and the premium story narration uses a separate higher-quality voice. Both are expensive to generate, so every clip is cached in Cloudflare R2 keyed by its content — the second person to hear a word gets it straight from the bucket — and generation is rate-limited through a Workers binding so nobody can run up the bill. On the page, characters are drawn with animated stroke order and pinyin is rendered on the fly, so a word page is a genuinely useful reference even before you sign up.

One Worker, edge-native

The whole application is a single Cloudflare Worker. TanStack Start handles server-side rendering and file-based routing, and a Hono app serves the /api/* routes from the very same Worker — one deploy, one runtime, SSR and API together. Data lives in Postgres, reached through Drizzle and fronted by Cloudflare Hyperdrive so a Worker at the edge gets pooled, low-latency connections to the database instead of paying a cold TCP handshake on every request. Auth is better-auth (email/password plus Google OAuth), billing is Polar wired in through better-auth, and product analytics run on PostHog.

Keeping it to one Worker is what makes the whole thing maintainable by one person. There's no separate API service to deploy, no gateway to keep in sync, no drift between a frontend repo and a backend repo — the types flow from the Drizzle schema out to the React components through a single codebase, so a change to the data model is felt everywhere at build time.

A reference site that doubles as the funnel

A large part of the app is deliberately public. Thousands of word pages, HSK lists spanning both the old 2.0 standard and the restructured 3.0 bands, idioms, songs with line-by-line pinyin, and a set of honest "alternative to" guides are all server-rendered and indexable. This is programmatic SEO doing double duty: every one of those pages is genuinely useful on its own, and every one is also a landing page for a web-first product where organic search is the primary channel. Dynamic Open Graph images are rendered per page so each of those thousands of URLs shares cleanly.

The interface itself runs on a small in-house design system — "Ink & Vermillion" — with its components developed and documented in Storybook and the whole app themeable light and dark. It's the kind of foundation that's easy to skip as a solo builder and pays for itself the moment the surface area grows past what you can hold in your head.

Outcome

MandarinAI is live at mandarin-ai.com, built and run single-handedly. The piece I'm proudest of isn't any one feature but the loop that ties them together: a scheduler that knows what you're forgetting, a generator that turns exactly those words into something worth reading, and an edge architecture lean enough that one person can keep all of it running. That closed loop — study becomes reading becomes study — is the thing no amount of raw LLM access gives you for free, and it's the whole reason the product exists.