← All work

Flashcards

Type a topic, pick 10, 20 or 30 cards, and Claude writes you a study deck. Flip through it right away or save it for the night before.

Year
2025
Made with
React, TypeScript, Material UI, Firebase Functions, Claude Sonnet 5.5, Stripe
Links
Live siteSource on GitHub
Flashcard Generator social card: 'Any topic. Instant flashcards.' beside a question card and an answer card about photosynthesis
Question, answer, done
Why

I built this to prep for job interviews: type in whatever the next one is likely to cover, get a deck, flip through it.

It was also the first project where I got Stripe subscriptions actually working, which is why FIT 9to5 started life as a copy of it.

How it works

Getting cards, not prose

The topic and card count go to an HTTPS Firebase Function that calls Claude Sonnet 5.5 with two tools. One is flash_cards, a custom tool whose input schema is just { cards: [{ question, answer }] }. It never runs anything; it exists so the model hands back structured output. The other is web search, capped at two uses, so decks on niche or recent topics rest on something real.

Tool use isn't forced, so parsing is defensive: take the flash_cards block if there is one, otherwise slice the first text block from the first [ to the last ] and parse that. The prompt also has to tell the model to keep citation tags out of the cards.

Who pays for the tokens

Generating needs an account, and the function verifies your Firebase ID token before anything else. Subscribers use my Anthropic key. People who saved their own key use theirs, which is stored encrypted and only ever shown as its last four characters. Everyone else gets one deck on me.

That free deck used to be a flag in localStorage, which is an honor system. Now it's claimed atomically in a server-only field, freeGenerationUsed, so two tabs can't both win, and it's handed back if the Anthropic call fails.

Subscriptions

Stripe hosts checkout, with the Firebase uid riding along as client_reference_id. The webhook verifies Stripe's signature against the raw request body and records the outcome in subscriptionStatus. The UI only ever reads that field, so every device and tab agrees.

Subscribe→createCheckoutSession→Stripe Checkout→handleStripeWebhook→subscriptionStatus
Screens
Flashcard Generator home page: 'Any topic. Instant flashcards.' above a topic field with suggested topics and a 10, 20 or 30 card-count switch
One field, a few suggestions, one button
What it taught me
  1. 1

    Give the model a schema, then parse defensively anyway

    Tool use isn't forced, so the bracket-slicing fallback stays.

  2. 2

    Web search brings citations you didn't ask for

    The prompt has to keep cite tags out of the card text.

  3. 3

    CORS will eat a day

    Getting Stripe Checkout past it took six commits and one revert.

  4. 4

    Let the webhook own the truth

    One status field, written only by the server, made every session agree.

Stack
FrontendReact 18, TypeScript, Material UI 5, React Router 6
Auth and dataFirebase Auth (email or Google), Realtime Database
BackendFirebase Functions v2, TypeScript, Node 22
ModelClaude Sonnet 5.5 with a custom tool and web search
PaymentsStripe Checkout in subscription mode, plus a webhook
HostingVercel, deploying from main