# How to Add Personalized Flashcards to Your App (Copy-Paste Prompts Included)

- Tool: App Features
- Last updated: July 2026

## TL;DR

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.

## 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** (ui): Two-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.
- **Deck manager** (data): Supabase `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.
- **Spaced repetition engine** (backend): SM-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.
- **Study session UI** (ui): Full-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.
- **Progress tracker** (data): Supabase 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.
- **AI card generator** (service): Optional 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.

## 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:

```sql
create table public.decks (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete cascade not null,
  title text not null,
  description text,
  is_public boolean not null default false,
  created_at timestamptz not null default now()
);

create table public.cards (
  id uuid primary key default gen_random_uuid(),
  deck_id uuid references public.decks(id) on delete cascade not null,
  front_content text not null,
  back_content text not null,
  front_image_url text,
  back_image_url text,
  tags text[] default '{}',
  created_at timestamptz not null default now()
);

create table public.card_reviews (
  id uuid primary key default gen_random_uuid(),
  card_id uuid references public.cards(id) on delete cascade not null,
  user_id uuid references auth.users(id) on delete cascade not null,
  reviewed_at timestamptz not null default now(),
  rating int not null check (rating between 1 and 4),
  ease_factor float not null default 2.5,
  interval_days int not null default 1,
  next_due_date date not null default current_date + 1
);

create table public.user_study_streaks (
  user_id uuid primary key references auth.users(id) on delete cascade,
  last_studied_date date,
  current_streak int not null default 0
);

-- RLS
alter table public.decks enable row level security;
alter table public.cards enable row level security;
alter table public.card_reviews enable row level security;
alter table public.user_study_streaks enable row level security;

-- Decks: owner full access; anyone can read public decks
create policy "Decks owner access"
  on public.decks for all
  using (auth.uid() = user_id)
  with check (auth.uid() = user_id);

create policy "Decks public read"
  on public.decks for select
  using (is_public = true);

-- Cards: owner full access; anon can read cards in public decks
create policy "Cards owner access"
  on public.cards for all
  using (auth.uid() = (select user_id from public.decks where id = deck_id))
  with check (auth.uid() = (select user_id from public.decks where id = deck_id));

create policy "Cards public deck read"
  on public.cards for select
  using ((select is_public from public.decks where id = deck_id) = true);

-- Card reviews: each user sees and inserts only their own rows
create policy "Card reviews user access"
  on public.card_reviews for all
  using (auth.uid() = user_id)
  with check (auth.uid() = user_id);

-- Streaks: user's own row only
create policy "Study streaks user access"
  on public.user_study_streaks for all
  using (auth.uid() = user_id)
  with check (auth.uid() = user_id);

-- Indexes
create index card_reviews_due_idx
  on public.card_reviews (user_id, next_due_date);

create index cards_deck_idx
  on public.cards (deck_id);
```

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 paths

### Lovable — fit 4/10, 1-2 days

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.

1. Create a new Lovable project, connect Lovable Cloud so the Supabase tables are provisioned automatically, then paste the prompt below in Agent Mode
2. After 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'
3. Test 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'
4. If 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

Starter prompt:

```
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.
```

Limitations:

- 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

### V0 — fit 4/10, 1-2 days

V0's Next.js + shadcn/ui generates a clean, component-organized flashcard app. Best when the flashcard feature is part of a larger Next.js product.

1. Prompt V0 with the full spec below, specifying Next.js App Router, shadcn/ui components, and Supabase for persistence
2. Add your Supabase credentials in the Vars panel (NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY); run the SQL schema from this page in the Supabase SQL editor
3. Test the study session locally — the V0 sandbox does not block keyboard events, so Space and digit shortcuts can be validated in the preview
4. For SM-2 correctness, rate a card 'again' three times and verify that next_due_date in Supabase is always tomorrow, never today or in the past

Starter prompt:

```
Build a personalized flashcard feature in Next.js App Router with Supabase and shadcn/ui. Components: (1) DeckList (server component) — fetch decks for the signed-in user from Supabase with card count and today's due count (next_due_date <= today); render as card grid with title, stats badge, and Study button. (2) CardEditor (client component) — react-hook-form with zod; fields: front_content (required), back_content (required), tags (shadcn Combobox for multi-select); Markdown preview using react-markdown with remark-gfm that toggles on a Preview tab. (3) StudySession (client component) — full-screen flip card: container with perspective: 1000px, transform-style: preserve-3d; front div with rotateY(0deg), back div with rotateY(180deg); both with backface-visibility: hidden; flip on Space keydown; after flip show four rating buttons (Again/Hard/Good/Easy); SM-2 function: new_ef = ease_factor + 0.1 - (5 - rating) * (0.08 + (5 - rating) * 0.02), clamp to min 1.3; new interval: rating=1 -> 1 day, rating=2 -> max(interval*1.2, 6) days, rating>=3 -> interval*new_ef days; upsert to card_reviews and fetch next due card. States: empty deck, all-caught-up with confetti, mid-session counter. Use Server Actions for deck and card mutations. Study session is a client component only.
```

Limitations:

- Supabase tables must be created manually using the SQL schema from this page — V0 does not auto-provision the database
- SM-2 algorithm description must be included in your prompt exactly as above — V0 has no built-in spaced repetition library and will produce a simplified version without the formula
- Framer Motion card flip (if preferred over CSS) must be explicitly requested; the default CSS approach works but requires correct transform-style: preserve-3d on the parent
- localStorage persistence for draft card content is not included by default — add a follow-up prompt if you want in-progress card edits to survive page refresh

### Custom — fit 3/10, 1-2 weeks

Custom development is justified when the flashcard feature extends beyond standard SM-2: Anki .apkg import/export, collaborative deck editing, voice study mode, or a certification exam engine.

1. React + Supabase with the SM-2 algorithm as a shared utility function tested with Vitest unit tests covering all edge cases (ease_factor floor, rating=1 interval reset, etc.)
2. Anki .apkg import: parse the SQLite-based .apkg ZIP file using sql.js in a Web Worker, map Anki's scheduling fields to your SM-2 schema
3. Voice study mode: Web Speech API for TTS (read card front aloud) and SpeechRecognition for capturing the answer, scored against back content with fuzzy matching (Fuse.js)
4. FSRS algorithm (newer than SM-2, used by Anki 23+) for higher retention accuracy — openly documented but significantly more complex to implement and tune

Limitations:

- SM-2 is freely documented and not complex — custom dev adds weeks without proportional user value for a standard study feature
- The Anki .apkg format is underdocumented; edge cases in media attachments and LaTeX rendering add significant scope
- Voice mode via Web Speech API is unreliable on Firefox and requires HTTPS; iOS Safari has inconsistent SpeechRecognition support

## Gotchas

- **SM-2 produces negative intervals after repeated 'again' ratings** — 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** — 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** — 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** — 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

## 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.

---

Source: https://www.rapidevelopers.com/app-features/personalized-flashcards
© RapidDev — https://www.rapidevelopers.com/app-features/personalized-flashcards
