Feature spec
IntermediateCategory
forms-data
Build with AI
1-2 days with Lovable or V0 + Supabase
Custom build
1-2 weeks custom dev
Running cost
$0/mo up to ~1,000 users; $25/mo at 10K users
Works on
Everything it takes to ship Personalized Flashcards — parts, prompts, and real costs.
A personalized flashcard system needs four pieces: a card editor, a deck manager, a spaced repetition engine (SM-2 algorithm), and a study session UI. With Lovable or V0 you can ship a working system in 1-2 days for roughly $0/month at small scale — Supabase free tier covers everything up to ~1,000 users. The only optional cost is AI card generation via Claude API at roughly $0.04 per 100 cards.
What a Personalized Flashcard Feature Actually Is
A personalized flashcard system lets users build and study their own card decks — front and back, text plus optional images — with a scheduling algorithm that decides which cards to show next. The key product decision is the repetition model: a simple two-bucket system (know/don't know) is trivial to build, while the SM-2 algorithm (used by Anki) schedules each card based on difficulty ratings and prior performance, dramatically reducing study time needed for retention. The second decision is scope: a private study tool is 80% less work than a collaborative or public deck platform. Get those two calls right before you start prompting.
What users consider table stakes in 2026
- Card flip animation (CSS rotateY) that reveals the back face on tap or spacebar press
- Spaced repetition scheduling shows overdue and today's due cards first, not random order
- Keyboard shortcuts in study mode: Space to flip, 1/2/3/4 for difficulty rating after reveal
- Progress indicator per deck showing mastered percentage and cards due today
- Study streak counter displayed on the home screen or deck list
- Empty-state screens for new decks, all-caught-up sessions, and first-time users that prompt the next action
Anatomy of the Feature
Six components. The CRUD layer and study UI are straightforward; the SM-2 engine and the RLS for public decks are where first builds fail.
Card editor
UITwo-panel form (front and back) built with react-hook-form. Supports plain text and Markdown via react-markdown with remark-gfm — critical for technical flashcards with code snippets. Optional image upload per card side stores files in Supabase Storage and saves the public URL in the cards row. Inline tag input using a shadcn/ui combobox.
Note: Keep front and back in separate TEXT columns, not JSONB — it simplifies queries and avoids schema migration headaches when you add images later.
Deck manager
DataSupabase `decks` table holds metadata (title, description, is_public). The `cards` table holds front_content, back_content, front_image_url, back_image_url, and tags. The deck list page shows card count and today's due count via a Supabase aggregate query joining card_reviews.
Note: Adding a `card_count` cached column to decks avoids an expensive COUNT on every list render once decks grow large.
Spaced repetition engine
BackendSM-2 algorithm implemented as a pure client-side JavaScript function or a Supabase Postgres function. Inputs: the card's current ease_factor (default 2.5) and interval_days (default 1), plus the user's rating (1=again, 2=hard, 3=good, 4=easy). Outputs: new ease_factor and interval_days, which set the next_due_date in card_reviews.
Note: Clamp ease_factor to a minimum of 1.3 — AI-generated SM-2 implementations frequently omit this guard, causing the algorithm to produce negative intervals after repeated 'again' ratings.
Study session UI
UIFull-screen card flip using CSS transform: rotateY(180deg) with React state toggle. A useEffect registers keyboard event listeners for Space (flip) and digit keys 1-4 (rate). After each rating, the SM-2 function runs client-side, the result is upserted to card_reviews, and the next due card is fetched from Supabase. Session ends when no more due cards remain.
Note: Set backface-visibility: hidden and perspective: 1000px on the card container — without these the back face shows through during the flip transition.
Progress tracker
DataSupabase aggregate query counts cards by last rating bucket (again/hard/good/easy) per deck. Mastered percentage = cards where ease_factor > 2.5 AND interval_days > 21. Study streak is tracked in a user_study_streaks table: last_studied_date and current_streak, updated after each completed session.
Note: Define 'streak' clearly upfront — does reviewing 1 card count, or must all due cards be completed? This decision determines the UX expectation and the update logic.
AI card generator
ServiceOptional Supabase Edge Function that calls the Anthropic API (claude-haiku-4-5) with a topic prompt and returns a JSON array of {front, back} pairs. The user reviews generated cards in a staging view before accepting them into a deck. Rate-limited per user per day to prevent abuse.
Note: claude-haiku-4-5 at $0.08/$0.25 per million tokens in/out. A typical generation prompt producing 20 cards is roughly 500 tokens — about $0.04 per 100 cards generated.
The data model
Four tables cover decks, cards, review history, and study streaks. Run this in the Supabase SQL editor — it sets up RLS so owners control their content and anonymous users can study public decks:
1create table public.decks (2 id uuid primary key default gen_random_uuid(),3 user_id uuid references auth.users(id) on delete cascade not null,4 title text not null,5 description text,6 is_public boolean not null default false,7 created_at timestamptz not null default now()8);910create table public.cards (11 id uuid primary key default gen_random_uuid(),12 deck_id uuid references public.decks(id) on delete cascade not null,13 front_content text not null,14 back_content text not null,15 front_image_url text,16 back_image_url text,17 tags text[] default '{}',18 created_at timestamptz not null default now()19);2021create table public.card_reviews (22 id uuid primary key default gen_random_uuid(),23 card_id uuid references public.cards(id) on delete cascade not null,24 user_id uuid references auth.users(id) on delete cascade not null,25 reviewed_at timestamptz not null default now(),26 rating int not null check (rating between 1 and 4),27 ease_factor float not null default 2.5,28 interval_days int not null default 1,29 next_due_date date not null default current_date + 130);3132create table public.user_study_streaks (33 user_id uuid primary key references auth.users(id) on delete cascade,34 last_studied_date date,35 current_streak int not null default 036);3738-- RLS39alter table public.decks enable row level security;40alter table public.cards enable row level security;41alter table public.card_reviews enable row level security;42alter table public.user_study_streaks enable row level security;4344-- Decks: owner full access; anyone can read public decks45create policy "Decks owner access"46 on public.decks for all47 using (auth.uid() = user_id)48 with check (auth.uid() = user_id);4950create policy "Decks public read"51 on public.decks for select52 using (is_public = true);5354-- Cards: owner full access; anon can read cards in public decks55create policy "Cards owner access"56 on public.cards for all57 using (auth.uid() = (select user_id from public.decks where id = deck_id))58 with check (auth.uid() = (select user_id from public.decks where id = deck_id));5960create policy "Cards public deck read"61 on public.cards for select62 using ((select is_public from public.decks where id = deck_id) = true);6364-- Card reviews: each user sees and inserts only their own rows65create policy "Card reviews user access"66 on public.card_reviews for all67 using (auth.uid() = user_id)68 with check (auth.uid() = user_id);6970-- Streaks: user's own row only71create policy "Study streaks user access"72 on public.user_study_streaks for all73 using (auth.uid() = user_id)74 with check (auth.uid() = user_id);7576-- Indexes77create index card_reviews_due_idx78 on public.card_reviews (user_id, next_due_date);7980create index cards_deck_idx81 on public.cards (deck_id);Heads up: The card_reviews_due_idx index is important — the study session query filters by user_id and next_due_date on every card fetch, and at 10K users with 50 daily reviews per user the table grows to 500K rows per day. Add a cron job or pg_cron task to archive reviews older than 90 days to a cold table.
Build it — pick your path
Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.
Lovable handles the CRUD layer, study session UI, and Supabase schema in one project. The SM-2 algorithm and card flip animation need explicit prompting but are within its competency.
Step by step
- 1Create a new Lovable project, connect Lovable Cloud so the Supabase tables are provisioned automatically, then paste the prompt below in Agent Mode
- 2After the first generation, open the study session preview and verify the card flip animation — if the back face shows through during rotation, add a follow-up prompt: 'Add backface-visibility: hidden and perspective: 1000px to the card container'
- 3Test SM-2 by rating a card 'again' five times in a row and checking that next_due_date is still 1 day, not negative — if not, prompt: 'Clamp ease_factor to a minimum of 1.3 in the SM-2 function'
- 4If adding AI card generation, set the Anthropic API key in Cloud tab → Secrets, then prompt for the Edge Function that calls claude-haiku-4-5 and returns JSON card pairs
Build a personalized flashcard study app. Supabase tables: decks (id, user_id, title, description, is_public boolean), cards (id, deck_id, front_content text, back_content text, tags text[]), card_reviews (id, card_id, user_id, rating int 1-4, ease_factor float default 2.5, interval_days int default 1, next_due_date date), user_study_streaks (user_id, last_studied_date, current_streak int). RLS: deck owners have full access; anyone can SELECT cards and decks where is_public=true. Pages: (1) Deck list — shows all my decks with card count and today's due count, plus a Create Deck button. (2) Deck detail — card list with Add Card form (front/back text fields with react-hook-form, tags combobox, Markdown preview toggle using react-markdown). (3) Study session — full-screen card flip with CSS rotateY(180deg) and perspective: 1000px; backface-visibility: hidden on both faces; Space key flips, digit keys 1-4 rate; SM-2 algorithm updates ease_factor (clamp min 1.3), interval_days, next_due_date after each rating; session ends with a 'All caught up' celebration screen when no due cards remain; show card counter '12 of 34' during session. Handle: empty deck state prompting to add first card; no-cards-due state with next due time shown; keyboard shortcuts only active when study session is focused.
Where this path bites
- tsvector full-text search across card content must be set up manually in the Supabase SQL editor — Lovable cannot add Postgres triggers via prompts
- Nested folder organization for decks requires a second prompt and sometimes produces an incorrect recursive component — prompt for flat folder structure first, then add nesting
- Card image uploads need a separate prompt to wire Supabase Storage correctly; the initial build will likely omit image_url fields
- AI card generation via Edge Function requires Secrets setup in the Cloud tab plus an additional prompt — it will not be included in the first build
Third-party services you'll need
The flashcard core requires no paid services. AI card generation is the only optional cost:
| Service | What it does | Free tier | Paid from |
|---|---|---|---|
| Supabase | Deck, card, and review history storage; optional image storage for card images | Free (2 projects, 500MB DB, 1GB Storage) | $25/mo Pro (8GB DB, 100GB Storage) |
| react-markdown + remark-gfm | Markdown rendering in card front/back content | Free, open-source (MIT) | Free |
| Anthropic Claude API (optional) | AI card generation from a topic prompt via claude-haiku-4-5; returns JSON array of {front, back} pairs | No free tier — pay per token | $0.08/$0.25 per M tokens in/out (approx) — roughly $0.04 per 100 cards generated |
Swipe the table sideways to see pricing.
What it costs to run
Drag through the tiers to see how your monthly bill scales with users — no surprises later.
Estimated monthly running cost
Supabase free tier covers deck, card, and review storage comfortably. All libraries are free. AI generation at this scale is a few cents total.
Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.
What breaks when AI tools build this
The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.
SM-2 produces negative intervals after repeated 'again' ratings
Symptom: The standard SM-2 ease_factor formula can decrease below 1.3 with no lower bound, producing next_due_date in the past. AI-generated implementations almost never include the clamp. The symptom is a card that keeps appearing every session no matter how often it is studied.
Fix: Add a clamp in the SM-2 function: ease_factor = Math.max(1.3, newEaseFactor). In the Postgres function version, use GREATEST(1.3, new_ef). Cards with ease_factor at the floor will be shown again in 1 day, which is the correct Anki behavior for difficult cards.
Card flip shows back content before animation completes
Symptom: React renders both the front and back faces in the DOM. Without backface-visibility: hidden on both .card-front and .card-back, the back face is visible through the front during the rotateY transition, creating a flash of the answer before the user clicks flip.
Fix: Set backface-visibility: hidden on both card faces. Ensure the card container has perspective: 1000px and transform-style: preserve-3d. The back face starts at rotateY(180deg); flipping the container to rotateY(180deg) reveals it. Test in Chrome DevTools with animation slow-motion to verify.
Due cards don't appear until the next day in some timezones
Symptom: SM-2 stores next_due_date as a DATE in UTC. A user in UTC-8 opens the app at 8pm local time — it is already 4am UTC tomorrow. Cards due 'today' in their timezone were due 'yesterday' in UTC, so they don't appear. Alternatively, cards scheduled for UTC-tomorrow are already past-due in the user's morning.
Fix: Query due cards with next_due_date <= current_date (UTC server time) and display this as 'cards due today' without surfacing the UTC detail. Alternatively, store next_due_date as TIMESTAMPTZ with a specific time (midnight UTC) and compare against NOW(). Avoid computing dates on the client — always use server UTC as the reference.
Public deck RLS returns empty cards for anonymous visitors
Symptom: If the cards table RLS policy only checks auth.uid() = (SELECT user_id FROM decks WHERE id = deck_id), unauthenticated users studying a public deck get zero results — the join-based check fails for null auth.uid(). The study session loads with no cards and shows the all-caught-up screen immediately.
Fix: Add a separate SELECT policy on cards: USING ((SELECT is_public FROM public.decks WHERE id = deck_id) = true). This allows anonymous reads specifically for cards belonging to public decks, without opening any other data.
Best practices
Always implement the SM-2 ease_factor floor of 1.3 — omitting it is the most common first-build defect and produces cards stuck in a daily loop
Fetch only due cards (next_due_date <= today) for the study session query, never all cards — at scale the difference in query time is an order of magnitude
Render Markdown in card content from the start, even if your initial use case is plain text — founders always want code blocks and bold formatting within two weeks of launch
Show card count and due count on the deck list, not just the deck title — this is the primary motivator that brings users back daily
Persist the study session state in localStorage so a refresh or accidental navigation does not lose the current session position
Add a version key to any localStorage cache (e.g., wizard_flashcards_v2) so schema changes in the card data model don't cause crashes when old cached data is read
Rate-limit the AI card generation endpoint per user per day — even at $0.04/100 cards, unlimited generation from one user can create a meaningful bill
Archive card_reviews rows older than 90 days — the table is the fastest-growing in the schema and routine cleanup prevents performance degradation at scale
When You Need Custom Development
AI tools handle the standard flashcard stack — deck CRUD, SM-2 scheduling, and study sessions — without issues. Custom development is warranted when the feature scope expands beyond these boundaries:
- Import and export of Anki .apkg files — the format is SQLite-based and underdocumented; correct media handling requires significant custom parsing logic
- Voice study mode where the app reads the card front aloud and scores a spoken answer against the back content using Web Speech API and fuzzy matching
- Collaborative deck editing: multiple teachers or authors editing the same deck simultaneously with conflict resolution
- Certification or timed exam mode with pass/fail grading, proctor controls, and audit-trail submission records — structurally different from spaced repetition
RapidDev builds these features for production
Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.
Frequently asked questions
How does spaced repetition work and how do I implement it in a web app?
Spaced repetition schedules each card based on how difficult it was last time. SM-2, the most widely used algorithm, adjusts two numbers per card: ease_factor (how easy the card is, default 2.5) and interval_days (how many days until the next review). After each rating (1=again, 2=hard, 3=good, 4=easy), the algorithm computes new values: new_ef = ease_factor + 0.1 - (5 - rating) * (0.08 + (5 - rating) * 0.02), clamped to a minimum of 1.3. The new interval is 1 day for 'again', or interval * ease_factor for 'good' and 'easy'. The card's next_due_date is set to today plus the new interval. Implement this as a pure JavaScript function and call it client-side after each card rating.
What Supabase table structure should I use for cards and review history?
You need at minimum three tables: decks (id, user_id, title, is_public), cards (id, deck_id, front_content, back_content), and card_reviews (id, card_id, user_id, rating, ease_factor, interval_days, next_due_date). The card_reviews table is the one that grows fastest — at 10K users studying 50 cards per day, you're adding 500K rows daily. Add a composite index on (user_id, next_due_date) from the start; this index makes the study session query instant even at millions of rows.
How do I build a card flip animation in React?
Use CSS 3D transforms: give the card container perspective: 1000px and transform-style: preserve-3d. Create two child divs (front face and back face), both with backface-visibility: hidden. The back face starts at rotateY(180deg). On flip, apply rotateY(180deg) to the container — the front disappears and the back becomes visible. Toggle this with React state. The critical mistake is omitting transform-style: preserve-3d on the container, which causes the back face to never appear.
How do I track which cards a user has mastered vs still learning?
Query the card_reviews table for each card's most recent row per user. Cards with ease_factor > 2.5 and interval_days > 21 are a reasonable definition of 'mastered' — this matches roughly a month of successful recalls. Display the percentage of mastered cards per deck on the deck list. Avoid updating a 'mastered' boolean column on the cards table — the definition of mastery may change and recalculating from card_reviews is more accurate.
Can I make decks public so other users can study them?
Yes. Add an is_public boolean to the decks table and set two Supabase RLS policies: one that allows all users (including anonymous) to SELECT decks where is_public = true, and one that allows the same for cards belonging to those public decks. The second policy is frequently missed — without it, anonymous visitors see the deck metadata but get zero cards in the study session.
How do I add AI-generated flashcards from a topic or document?
Create a Supabase Edge Function that accepts a topic string and calls the Anthropic API (claude-haiku-4-5) with a prompt like 'Generate 20 flashcards about [topic]. Return JSON array of {front, back} objects.' Parse the response and return the array to the client. Show the generated cards in a staging review screen where the user can remove unwanted cards before importing them to a deck. At $0.08 per million input tokens, a 20-card generation run costs well under a cent.
How do I prevent the study session from showing cards I already mastered?
The study session query should only fetch cards where next_due_date <= CURRENT_DATE (server UTC). Cards rated 'good' or 'easy' consistently will have next_due_date months in the future and simply won't appear. There is no need to mark cards as 'mastered' explicitly — SM-2 naturally removes them from daily rotation. If a user wants to force-study any card regardless of schedule, add a 'Browse mode' that fetches all cards in the deck without the due-date filter.
What's the difference between a 2-button and 4-button rating system?
A 2-button system (know/don't know) is easier for users to understand and halves the interaction time per card. Use it for consumer apps and simple vocabulary study. A 4-button system (again/hard/good/easy) is the full SM-2 experience — it produces better long-term scheduling because 'hard' cards are delayed less than 'again' cards. Use it when your users are students studying for exams or certifications who will use the app intensively. The SM-2 formula requires a 4-point rating scale; with 2 buttons, map 'know' to rating=4 and 'don't know' to rating=1.
Need this feature production-ready?
RapidDev builds personalized flashcards into real apps — auth, database, payments — at $13K–$25K.
Book a free consultation30-min call. No commitment.