Skip to main content
RapidDev - Software Development Agency

How to Build Language learning app with V0

Build a Duolingo-style language learning app with V0 using Next.js, Supabase, and shadcn/ui. Features spaced repetition flashcards with the SM-2 algorithm, progress streaks, deck browsing, and card flip animations — all with server-side scheduling logic to prevent cheating. Takes about 1-2 hours.

What you'll build

  • Flashcard review interface with CSS perspective transform flip animation and difficulty rating buttons
  • SM-2 spaced repetition algorithm running server-side to schedule optimal review intervals
  • Progress streak tracker with daily activity tracking and longest streak records
  • Public deck browser for discovering and studying community-created flashcard decks
  • Review dashboard showing due card counts, session progress, and streak flame indicator
  • Audio playback support for pronunciation using Supabase Storage public bucket
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate12 min read1-2 hoursV0 Premium or higherApril 2026RapidDev Engineering Team
TL;DR

Build a Duolingo-style language learning app with V0 using Next.js, Supabase, and shadcn/ui. Features spaced repetition flashcards with the SM-2 algorithm, progress streaks, deck browsing, and card flip animations — all with server-side scheduling logic to prevent cheating. Takes about 1-2 hours.

What you're building

Language learning apps are one of the most popular app categories, and spaced repetition is the scientifically proven method behind tools like Anki and Duolingo. Building your own gives you full control over the content, scheduling algorithm, and user experience.

V0 makes this feasible for non-developers by generating the flashcard UI, review logic, and deck management from prompts. Connect Supabase via the Connect panel for the database, auth, and audio file storage. Queue up prompts for the review engine, streak tracker, and deck browser to build all three features in one session.

The architecture uses a client component for the interactive flashcard flip, the SM-2 algorithm computed server-side in an API route to prevent manipulation, Supabase for progress tracking and deck storage, and Server Components for the dashboard and deck browser.

Final result

A spaced repetition language learning app with flashcard review sessions, SM-2 scheduling, streak tracking, public deck browsing, and audio pronunciation support.

Tech stack

V0AI Code Generator
Next.jsFull-Stack Framework
Tailwind CSSStyling
shadcn/uiComponent Library
SupabaseDatabase
Supabase StorageStorage

Prerequisites

  • A V0 account (Premium or higher for prompt queuing)
  • A Supabase project (free tier works — connect via V0's Connect panel)
  • Flashcard content for at least one language deck (front/back pairs)
  • Basic understanding of flashcard-based learning (no coding needed)

Build steps

1

Set up the database schema for decks, cards, and progress

Create the Supabase schema for languages, decks, cards, user progress with SM-2 fields, and streak tracking. The user_progress table stores the spaced repetition state for each card per user.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build a language learning app. Create a Supabase schema with these tables:
3// 1. languages: id (uuid PK), code (text), name (text)
4// 2. decks: id (uuid PK), language_id (uuid FK to languages), title (text), description (text), creator_id (uuid FK to auth.users), is_public (boolean DEFAULT false)
5// 3. cards: id (uuid PK), deck_id (uuid FK to decks), front (text), back (text), audio_url (text), image_url (text)
6// 4. user_progress: id (uuid PK), user_id (uuid FK to auth.users), card_id (uuid FK to cards), ease_factor (numeric DEFAULT 2.5), interval_days (int DEFAULT 1), repetitions (int DEFAULT 0), next_review_at (timestamptz), last_reviewed_at (timestamptz)
7// 5. streaks: user_id (uuid PK FK to auth.users), current_streak (int DEFAULT 0), longest_streak (int DEFAULT 0), last_active (date)
8// Add RLS policies: users can read public decks and their own progress.
9// Generate SQL migration and TypeScript types.

Pro tip: Use V0's prompt queuing to build the schema, review interface, and deck browser in sequence — queue up to 10 prompts without waiting.

Expected result: Supabase is connected with all five tables created. SM-2 fields (ease_factor, interval_days, repetitions, next_review_at) are ready for the algorithm.

2

Build the flashcard review interface with flip animation

Create the interactive review session page with a card flip animation, difficulty rating buttons, and session progress tracking. This is a 'use client' component because it needs click handlers and animation state.

components/review-session.tsx
1'use client'
2
3import { useState } from 'react'
4import { Card, CardContent } from '@/components/ui/card'
5import { Button } from '@/components/ui/button'
6import { Progress } from '@/components/ui/progress'
7
8interface FlashCard {
9 id: string
10 front: string
11 back: string
12 audio_url?: string
13}
14
15export function ReviewSession({ cards }: { cards: FlashCard[] }) {
16 const [index, setIndex] = useState(0)
17 const [flipped, setFlipped] = useState(false)
18 const current = cards[index]
19 const progress = ((index) / cards.length) * 100
20
21 async function handleRating(quality: number) {
22 await fetch('/api/review', {
23 method: 'POST',
24 headers: { 'Content-Type': 'application/json' },
25 body: JSON.stringify({ card_id: current.id, quality }),
26 })
27 setFlipped(false)
28 setIndex((i) => i + 1)
29 }
30
31 if (index >= cards.length) {
32 return (
33 <Card className="p-8 text-center">
34 <CardContent>
35 <h2 className="text-2xl font-bold">Session Complete!</h2>
36 <p className="text-muted-foreground mt-2">You reviewed {cards.length} cards</p>
37 </CardContent>
38 </Card>
39 )
40 }
41
42 return (
43 <div className="space-y-6">
44 <Progress value={progress} />
45 <div
46 className="cursor-pointer perspective-1000"
47 onClick={() => setFlipped(!flipped)}
48 >
49 <div className={`relative w-full h-64 transition-transform duration-500 transform-style-3d ${flipped ? 'rotate-y-180' : ''}`}>
50 <Card className="absolute inset-0 backface-hidden flex items-center justify-center p-8">
51 <CardContent className="text-2xl font-medium text-center">{current.front}</CardContent>
52 </Card>
53 <Card className="absolute inset-0 backface-hidden rotate-y-180 flex items-center justify-center p-8">
54 <CardContent className="text-2xl font-medium text-center">{current.back}</CardContent>
55 </Card>
56 </div>
57 </div>
58 {flipped && (
59 <div className="flex gap-2 justify-center">
60 <Button variant="destructive" onClick={() => handleRating(1)}>Again</Button>
61 <Button variant="outline" onClick={() => handleRating(3)}>Hard</Button>
62 <Button variant="default" onClick={() => handleRating(4)}>Good</Button>
63 <Button variant="secondary" onClick={() => handleRating(5)}>Easy</Button>
64 </div>
65 )}
66 </div>
67 )
68}

Expected result: The review session shows a flippable flashcard with difficulty rating buttons. Cards advance on rating, and a progress bar tracks session completion.

3

Implement the SM-2 spaced repetition algorithm server-side

Create the API route that receives a card ID and quality rating, computes the new SM-2 interval and ease factor, and updates the user's progress. Running this server-side prevents users from manipulating their review schedule.

app/api/review/route.ts
1import { createClient } from '@supabase/supabase-js'
2import { NextRequest, NextResponse } from 'next/server'
3
4const supabase = createClient(
5 process.env.SUPABASE_URL!,
6 process.env.SUPABASE_SERVICE_ROLE_KEY!
7)
8
9function sm2(quality: number, repetitions: number, easeFactor: number, interval: number) {
10 if (quality < 3) {
11 return { repetitions: 0, interval: 1, easeFactor }
12 }
13
14 let newInterval: number
15 if (repetitions === 0) newInterval = 1
16 else if (repetitions === 1) newInterval = 6
17 else newInterval = Math.round(interval * easeFactor)
18
19 const newEase = Math.max(
20 1.3,
21 easeFactor + (0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02))
22 )
23
24 return {
25 repetitions: repetitions + 1,
26 interval: newInterval,
27 easeFactor: newEase,
28 }
29}
30
31export async function POST(req: NextRequest) {
32 const { card_id, quality } = await req.json()
33
34 if (!card_id || quality === undefined || quality < 0 || quality > 5) {
35 return NextResponse.json({ error: 'Invalid input' }, { status: 400 })
36 }
37
38 const { data: progress } = await supabase
39 .from('user_progress')
40 .select('*')
41 .eq('card_id', card_id)
42 .single()
43
44 const current = progress || { repetitions: 0, ease_factor: 2.5, interval_days: 1 }
45 const result = sm2(quality, current.repetitions, current.ease_factor, current.interval_days)
46
47 const nextReview = new Date()
48 nextReview.setDate(nextReview.getDate() + result.interval)
49
50 const { error } = await supabase.from('user_progress').upsert({
51 card_id,
52 user_id: (await supabase.auth.getUser()).data.user?.id,
53 ease_factor: result.easeFactor,
54 interval_days: result.interval,
55 repetitions: result.repetitions,
56 next_review_at: nextReview.toISOString(),
57 last_reviewed_at: new Date().toISOString(),
58 })
59
60 if (error) return NextResponse.json({ error: error.message }, { status: 500 })
61 return NextResponse.json({ next_review_at: nextReview.toISOString(), interval: result.interval })
62}

Expected result: The API route computes SM-2 scheduling and updates the next review date. Cards rated 'Again' reset to 1-day interval; 'Easy' cards get progressively longer intervals.

4

Build the learn dashboard with streak tracking

Create the main dashboard showing due card counts per deck, current streak, and quick-start buttons for review sessions. The streak updates automatically when users complete at least one review per day.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Create a learn dashboard at app/learn/page.tsx.
3// Requirements:
4// - Fetch due cards count (where next_review_at <= now) grouped by deck
5// - Show streak info in a Card: flame icon, current_streak number, 'day streak' text, longest_streak below
6// - Show a grid of deck Cards with: deck title, language Badge, due cards count in a red Badge, total cards, Progress bar for mastered percentage
7// - Each deck Card has a 'Review Now' Button linking to /learn/[deckId]
8// - If no cards are due, show a celebration message: 'All caught up! Come back tomorrow.'
9// - Add a 'Browse Decks' Button linking to /decks for discovering new content
10// - Update the streak: if last_active is yesterday, increment current_streak; if last_active is not yesterday or today, reset to 1; if today, do nothing

Pro tip: The streak logic runs as a Server Action when the dashboard loads. Compare last_active date with today to determine whether to increment, reset, or skip the streak update.

Expected result: The dashboard shows due card counts, streak tracking with flame icon, and quick-start review buttons for each deck.

5

Create the public deck browser and deploy

Build a browse page where users can discover public decks, preview cards, and add decks to their study list. Then configure environment variables and publish to production.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Create a deck browser at app/decks/page.tsx.
3// Requirements:
4// - Fetch all public decks from Supabase with card count and language name
5// - Display as a shadcn/ui Card grid: deck title, language Badge, card count, description preview, creator name
6// - Add search Input for filtering by title or language
7// - Add a Drawer for deck detail: shows first 5 cards (front only), full description, and a 'Start Learning' Button
8// - 'Start Learning' copies the deck to the user's progress and redirects to /learn/[deckId]
9// - Use Server Components for the page with Supabase query, client component only for the search input and Drawer interaction
10// Also create a Supabase Storage public bucket called 'audio' for pronunciation files.

Expected result: The deck browser shows public decks with search filtering. Users can preview decks and start learning. The app is published to Vercel.

Complete code

app/api/review/route.ts
1import { createClient } from '@supabase/supabase-js'
2import { NextRequest, NextResponse } from 'next/server'
3
4const supabase = createClient(
5 process.env.SUPABASE_URL!,
6 process.env.SUPABASE_SERVICE_ROLE_KEY!
7)
8
9function sm2(
10 quality: number,
11 repetitions: number,
12 easeFactor: number,
13 interval: number
14) {
15 if (quality < 3) {
16 return { repetitions: 0, interval: 1, easeFactor }
17 }
18 let newInterval: number
19 if (repetitions === 0) newInterval = 1
20 else if (repetitions === 1) newInterval = 6
21 else newInterval = Math.round(interval * easeFactor)
22
23 const newEase = Math.max(
24 1.3,
25 easeFactor + (0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02))
26 )
27 return { repetitions: repetitions + 1, interval: newInterval, easeFactor: newEase }
28}
29
30export async function POST(req: NextRequest) {
31 const { card_id, quality, user_id } = await req.json()
32
33 if (!card_id || quality < 0 || quality > 5) {
34 return NextResponse.json({ error: 'Invalid input' }, { status: 400 })
35 }
36
37 const { data: progress } = await supabase
38 .from('user_progress')
39 .select('*')
40 .eq('card_id', card_id)
41 .eq('user_id', user_id)
42 .single()
43
44 const prev = progress || { repetitions: 0, ease_factor: 2.5, interval_days: 1 }
45 const result = sm2(quality, prev.repetitions, prev.ease_factor, prev.interval_days)
46 const nextReview = new Date()
47 nextReview.setDate(nextReview.getDate() + result.interval)
48
49 await supabase.from('user_progress').upsert({
50 user_id,
51 card_id,
52 ease_factor: result.easeFactor,
53 interval_days: result.interval,
54 repetitions: result.repetitions,
55 next_review_at: nextReview.toISOString(),
56 last_reviewed_at: new Date().toISOString(),
57 })
58
59 return NextResponse.json({ next_review_at: nextReview, interval: result.interval })
60}

Customization ideas

Text-to-speech pronunciation

Integrate the Web Speech API or ElevenLabs for real-time pronunciation of the card front text, letting users hear words without pre-recorded audio files.

Sentence builder exercises

Add a drag-and-drop sentence construction mode where users arrange word tiles in the correct order, building on the flashcard vocabulary.

Leaderboard and social features

Create a weekly leaderboard ranking users by cards reviewed, streak length, or XP points earned from correct answers.

Deck creation wizard

Add a multi-step form for creating custom decks with bulk card import from CSV, image upload per card, and audio recording via the browser microphone.

Common pitfalls

Pitfall: Running the SM-2 algorithm on the client side

How to avoid: Compute the SM-2 algorithm in an API route (server-side). The client only sends the card_id and quality rating; the server calculates the new interval and updates the database.

Pitfall: Querying all cards instead of only due cards for review

How to avoid: Filter cards with .lte('next_review_at', new Date().toISOString()) to only fetch cards that are due for review today or earlier.

Pitfall: Using window or localStorage in the Server Component for streak data

How to avoid: Store streak data in Supabase, not localStorage. The Server Component fetches it from the database. For client-only features, use 'use client' directive or dynamic import with { ssr: false }.

Best practices

  • Run the SM-2 algorithm server-side in an API route to prevent users from manipulating their review schedule
  • Use Supabase Storage public bucket for audio files so they load quickly without signed URL overhead
  • Use V0's prompt queuing to build the review engine, streak tracker, and deck browser in sequence without waiting between prompts
  • Filter due cards with .lte('next_review_at', now) to only show cards that need review today — this is the core of spaced repetition
  • Store streak data in Supabase rather than localStorage to persist across devices and prevent manipulation
  • Use CSS perspective transforms for the card flip animation — it is GPU-accelerated and smooth on mobile devices
  • Add a unique constraint on (user_id, card_id) in user_progress to ensure one progress record per card per user
  • Use V0's Design Mode (Option+D) to adjust card sizes, button colors, and spacing for the review interface without spending credits

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building a spaced repetition language learning app with Next.js and Supabase. I need help implementing the SM-2 algorithm. The function should take a quality rating (0-5), current repetitions count, ease factor (default 2.5), and current interval in days. It should return the new repetitions, interval, and ease factor. Quality < 3 means reset. Please write the TypeScript function and explain the math.

Build Prompt

Create a flashcard flip component using CSS 3D transforms (perspective, rotateY, backface-visibility). The card should flip on click revealing the answer on the back. Below the flipped card, show four difficulty buttons: Again (red), Hard (outline), Good (default), Easy (secondary). Track the current card index and show a Progress bar for session completion. When all cards are reviewed, show a completion Card with stats.

Frequently asked questions

What is the SM-2 spaced repetition algorithm?

SM-2 is the algorithm used by Anki and similar apps. It adjusts review intervals based on how well you know a card. Cards you rate as 'Easy' are shown less frequently (longer intervals), while cards you rate as 'Again' reset to a 1-day interval. The ease factor adjusts over time to personalize the schedule.

Can users create and share their own decks?

Yes. The decks table has a creator_id field and an is_public boolean. Users create decks and cards through the creation form, and can toggle them as public. Public decks appear in the deck browser for other users to discover and study.

How does the streak tracking work?

The streaks table stores current_streak, longest_streak, and last_active date. When a user opens the dashboard, a Server Action compares last_active with today. If yesterday, it increments the streak. If more than one day ago, it resets to 1. If today, it does nothing.

Do I need a paid V0 plan?

Premium ($20/month) is recommended. The language learning app has multiple interactive components (flashcard flip, review session, deck browser, dashboard) that require several prompts to build. Prompt queuing on Premium lets you build faster.

Can I add audio pronunciation to flashcards?

Yes. Upload audio files to a Supabase Storage public bucket. The cards table has an audio_url field. In the review interface, add an audio play button that uses the HTML5 Audio API to play the pronunciation when the card is shown.

How do I deploy the language learning app?

Click Share in V0, then Publish to Production. Set NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY in the Vars tab for client-side card fetching. The app deploys to Vercel in 30-60 seconds.

Can RapidDev help build a custom language learning app?

Yes. RapidDev has built over 600 apps including educational platforms with spaced repetition, gamification, and multi-language support. Book a free consultation to discuss your learning methodology and get a production-ready app.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help building your app?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.