← Back to projects

Flashcard App

Finished
Next.jsTypeScriptStripeAI IntegrationTursoSpaced RepetitionFlashcardsGamification
Flashcard App logo

Flashcard App - Technical Documentation

Overview

The Flashcard App (these days simply called Learn) is a web application for effective learning built on top of the Spaced Repetition System (SRS). Built with Next.js 16, TypeScript, and Turso database, it started as a classic flashcard app and has since turned into something closer to "Duolingo meets Quizlet": you bring your material, and the app turns it into short, varied exercises.

What started as a quick project for my final general education exam turned into something much more ambitious. The app implements a proper SRS algorithm (borrowed heavily from Anki's approach), supports multiple languages, includes AI-powered generation that actually works pretty well, has a complete subscription system with Stripe integration, and since version 2.0 also has nine item types, twelve kinds of exercises, XP, streaks and achievements.

Honestly, it was already over-engineered for what I initially needed back at version 1.11. Version 2.0 did not exactly fix that, and version 3.0 followed on the same day with a full dependency migration to Next.js 16. But where's the fun in building something simple?

Learn landing page

Project History & Vision

The Idea

The whole thing started because I needed to study for my final exams and wasn't particularly happy with the existing flashcard apps out there. Anki is powerful but feels ancient, and most modern alternatives either cost too much, are bloated with ads, or lack the sophisticated spaced repetition that actually makes flashcards effective.

So naturally, instead of just using what was available, I decided to build my own. It was the perfect opportunity to procrastinate a little longer before I actually had to start studying.

Development Journey

Initially, this was meant to be a quick proof of concept - just enough to get through my exams. But as often happens with side projects, it grew legs and started walking on its own.

The first version was pretty rough. I had database logic scattered throughout components, inconsistent statistics calculations, and way too many console.log statements that I forgot to remove.

Then I got carried away and added AI-powered flashcard generation. Because apparently what a simple flashcard app really needed was machine learning integration. The AI stuff was initially 100% generated by AI (ironic, I know), but it actually did a surprisingly good job.

The subscription system came next when I realized that AI API calls aren't free, and neither is hosting. So now there's a whole Stripe integration with payment recovery emails and billing portals. Because nothing says "simple study tool" like a complete SaaS business model.

That was roughly where the last version of this page ended (1.11.0, July 2025). Here's what happened since then.

The quiet months (1.12 - 1.14)

The second half of 2025 was mostly maintenance and "boring but important" work:

  • 1.12 added an AI disclaimer and, finally, running the test suite in GitHub Actions. Yes, before that the tests only ran when I remembered to run them.
  • 1.13 redesigned the dashboard with quick stats and fixed the mobile layout, which had a habit of overflowing on small screens.
  • 1.14 added rate limiting to basically every critical feature (not just AI and emails) and new signup emails that actually know the difference between "welcome" and "welcome back".

In December 2025 the critical React Server Components vulnerabilities were published, so there was a quick round of Next.js and React upgrades. Nothing exciting, just one of those "drop everything and update" evenings. In January 2026 the deck categories got a proper tag input with badges instead of a comma-separated text field.

The day the AI stopped working

In August 2026 the AI generation suddenly stopped working. No code change, no deployment, nothing. Turns out Google had retired gemini-1.5-flash, the model the app had been using since day one. Lesson learned: when you depend on someone else's model, "it works" has an expiry date. The quick fix was switching to a newer model, and that little scare is also what started me thinking about rebuilding the AI part properly.

Version 2.0: from flashcards to exercises

The longer I used the app, the more one thing bugged me: flipping a card and rating yourself from 1 to 4 is honestly not that much fun. It's also easy to lie to yourself. "Yeah, I totally knew that one. Good."

Apps like Duolingo solve this by making you do something with the answer: pick it, type it, match it, put it in order. And Quizlet shows that one set of material can power several study modes. So the idea for 2.0 was simple: keep the SRS core, but stop treating everything as a question and an answer.

That "simple" idea ended up touching 189 files, with roughly 24,000 lines added and 8,000 removed. It brought:

  • Nine item types instead of just front/back cards
  • Twelve kinds of exercises that the app picks for you
  • An adaptive learning engine that makes exercises harder as you get better
  • Four study modes per deck: Learn, Flashcards, Match and Practice test
  • Light gamification with XP, a daily goal, streaks with a freeze, and achievements
  • A rebuilt AI generator that creates mixed item types and reads full PDFs up to 20 MB
  • A redesigned landing page with every exercise playable right on the page, plus docs, help, roadmap and contact pages

A large part of 2.0 was written together with Claude Code. After how well the AI-generated parts of 1.x turned out, it felt like the natural next step. I still review everything, and I spent more time than I'd like to admit arguing about edge cases in the session logic, but it made a rewrite of this size possible as a side project.

Release management also got an update along the way: semantic-release was replaced by release-please, which keeps a release PR open with the next version and changelog. Merging that PR is how 2.0.0 shipped.

Version 3.0: the great dependency update

Two major versions on the same day. Thanks, semantic versioning.

With 2.0 out of the door, the dependencies were the next problem. Most of them were one or more major versions behind, and some of the upgrades depended on each other. So instead of upgrading package by package, I did everything in one go: 59 files changed, about 4,500 lines added and 3,900 removed. Because it requires a newer Node.js version and a new Stripe API version, it is technically a breaking change, so release-please made it 3.0.0. For users, nothing changed except that some things now work better on phones.

The big ones:

  • Next.js 15.4 → 16.4 - middleware.ts is now called proxy.ts (the codemod did that part), Turbopack is the default for next dev, next lint is gone in favor of the plain ESLint CLI with a flat config, and revalidateTag now wants a cache profile. Next.js 16 also type-checks the tests during the build, which found a few type errors that had been hiding there for a while.
  • React 19.3 with the new React Hooks lint rules (v7). They complained about impure calls and ref reads during render and about synchronous setState calls in effects. Fixing those made a few components noticeably cleaner.
  • AI SDK 4 → 7 and @ai-sdk/google 1 → 4 - The generator now uses generateText with Output.object instead of the old object API. Three major versions in one jump, plus new names for things like mediaType and maxOutputTokens.
  • Stripe 18 → 23 with a new API version. That's the scary one, because payments are the one thing you really don't want to break. The webhook endpoint had to be moved to the new API version as well.
  • Zod 4 - The zod schemas are the single source of truth for all item types, so the editor, the import and the AI output all depend on this upgrade.
  • And the rest - Vitest 5, ESLint 10, TypeScript 6, Tailwind 4.3, lucide 1.x, react-day-picker 10, Resend 6 and more. Node.js 22.12 is now the minimum, and CI runs on Node 24.

Not everything went to the latest version. TypeScript stays on 6 because TypeScript 7 doesn't have a JavaScript API yet that Next.js and typescript-eslint can use. And nodemailer stays on 7 because NextAuth v4 needs it. My favorite detail: lucide 1.x removed all brand icons, so the GitHub logo in the footer is now an inline SVG.

Right after the migration I also fixed the layout on phones. The landing page heading pushed the demo card off the screen in German, the deck editor squeezed its title to one letter per line, and German compound words like "Nutzungsbedingungen" simply overflowed. Headings now hyphenate based on the page language, and every page was checked at 320, 360, 375, 768 and 1280 px in both languages. Being German finally paid off as a test case.

Current State

As of version 3.0.0, the app includes:

  • Proper SRS implementation with the SuperMemo-2 algorithm
  • Nine item types and twelve exercise kinds, picked per item and mastery stage
  • Learn sessions, classic flashcards, a timed match game and practice tests
  • XP, daily goal, streak with freeze, mastery stages and achievements
  • AI generation of mixed item types from text or PDF documents (up to 20 MB)
  • Item editor for all types, image upload for diagrams, JSON import/export
  • Multi-language support (English and German)
  • Complete subscription system with Pro features and payment recovery
  • Next.js 16, React 19.3 and up-to-date dependencies across the board

It's deployed on Vercel and actually works pretty well. I use it regularly, along with a growing number of other users, which I guess is the ultimate test for any personal project.

Dashboard with streak, daily goal and XP

The dashboard after 2.0. Daily goal, streak and XP sit next to your decks.

Future Vision

The goal is still to create a learning app that doesn't suck. Something that combines the sophistication of Anki with a modern, intuitive interface, plus some AI magic to make creating good learning material easier.

2.0 added the part I was missing before: learning should feel a bit like play, without turning into a game that nags you. No hearts, no ads, no guilt-trip notifications. Just enough motivation to come back tomorrow.

Architecture Overview

High-Level Architecture

This is still a pretty standard Next.js application with server-side rendering and server actions. I chose to keep everything in a single codebase rather than splitting into separate frontend and backend services, mainly because hosting is so much easier when you only have one thing to deploy.

The architecture now looks something like this:

TXT
1┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ 2│ Frontend │ │ Server Actions │ │ Database │ 3│ (React/Next) │◄──►│ (Next.js) │◄──►│ (Turso) │ 4└─────────────────┘ └──────────────────┘ └─────────────────┘ 5 │ │ 6 ▼ ▼ 7┌─────────────────┐ ┌──────────────────┐ 8│ Pure logic │ │ External APIs │ 9│ lib/learn, │ │ Stripe, Gemini, │ 10│ lib/items │ │ Resend, Blob ... │ 11└─────────────────┘ └──────────────────┘

Component Structure

I tried to follow a pretty standard React pattern:

  • Pages handle routing and data fetching
  • Components are (mostly) reusable UI pieces
  • Server Actions handle all the business logic
  • Lib contains pure logic and integrations with external services

The biggest change in 2.0 is that the learning logic now lives in plain, framework-free TypeScript modules:

  • lib/items/ - zod schemas for all item types. They are the single source of truth for the editor, the JSON import and the AI output.
  • lib/learn/ - mastery stages, the exercise planner, the session queue, grading and XP. No React, no database, just functions.
  • lib/gamification/ - streaks (timezone-aware) and achievements.

Because these modules are pure, they are easy to test. That's a nice change from the 1.x days, where half of the logic was hiding inside components and I was too scared to touch it.

Data Flow

Data flows through server actions, which is honestly one of my favorite things about Next.js. No need to set up API routes for everything - you can just call server functions directly from your components.

A learn session works like this: a server action loads the deck and the SRS state, the pure session builder creates a queue of exercises, and the client runs through it. After every answer, the client calls submitExerciseResult, which grades the outcome and stores the new SRS state. At the end, completeSession adds the session bonus, updates the streak and checks for new achievements.

There is exactly one function that writes SRS state (applyReview in db/learn.ts). After the 1.x statistics mess, that rule was not up for discussion.

State Management

I keep client-side state pretty minimal. Most data lives in the database and gets fetched fresh when needed. For the few things that need client-side state (like user preferences, animations, and form states), I use Zustand with localStorage persistence.

One fun problem with 2.0: lots of exercises shuffle their options. Shuffle on the server, shuffle again on the client, and React greets you with a hydration mismatch. The fix was to render shuffled views only after the component has mounted. Not elegant, but it works.

Core Features

Deck Management

Users can create and organize items into decks. Each deck has:

  • Title and description
  • Tags for organization (a proper badge input since January 2026)
  • Optional exam date or deadline
  • Progress by mastery stage
  • AI generation capabilities (for Pro users)

Deck creation is now a small three-step flow: details, add items, learn. The deck page shows your progress and the four study modes.

Deck page with mastery progress and study modes

Each deck shows how many items are new, learning, familiar or mastered, and offers four ways to study.

Item Types

This is the core of 2.0. Not everything you want to learn is a question and an answer. Some things are lists, some are timelines, some are numbers, and some are spots on a map. So instead of "cards", the app now has items, and each item has one of nine types:

TypeWhat it isBecomes
Q&AA question and its answerMultiple choice, typing, flashcard
Multiple choiceA question with options, one or more correctMultiple choice
ClozeA sentence with gapsFill the blanks, typing
PassageText to learn word for word (quotes, laws)Build the sentence, first letters
ListA set to name in any orderName them all
SequenceItems in the right order (timelines, steps)Put in order
NumberA number or date, with optional toleranceMultiple choice, numbers
PairsTerms and their partnersMatch pairs
DiagramAn image with labeled spotsLabel a diagram, find the spot

In the database, every item still has a front (the prompt) and a back (a plain-text summary of the answer), plus a type and a JSON content column. Old cards simply became basic items, so the migration didn't have to touch any existing data. Backward compatibility for free, which is the best kind.

Item types and the exercises they turn into

Creating Items

There are now several ways to add items:

  1. The editor - Pick an item type and fill out the fields. Diagram items get an image upload (stored on Vercel Blob).
  2. JSON import - Paste an array of items, up to 200 at once. Classic { front, back } cards from 1.x still work.
  3. AI generation from text - Describe what you want to learn, and the AI creates mixed items.
  4. AI generation from PDF - Upload a document, and the AI turns it into items.

All four paths go through the same zod schema. If it doesn't validate, it doesn't get saved, no matter if a human or an AI wrote it.

Item editor with all nine item types

Exercises

Each item type turns into exercises that fit it. In total there are twelve kinds: multiple choice, type the answer, match pairs, fill the blanks, put in order, name them all, numbers, label a diagram, find the spot, first letters, build the sentence, and the classic flashcard.

Some of my favorites:

  • Name them all works like a Sporcle quiz. Type the members of a set in any order until you have them all (or give up).
  • First letters is for passages you need word for word. You only type the first letter of each word, which is surprisingly effective for memorizing.
  • Numbers accepts answers within a tolerance and gives you "higher" or "lower" hints.
  • Find the spot shows an image and you tap where something is. The closer, the better.

Small typos are forgiven when you type an answer, and you can mark an answer as correct if the app was too strict. Nobody likes losing a streak over a missing accent.

Four of the twelve exercise kinds

Label a diagram, put in order, name them all and fill the blanks. All of them are also playable on the landing page.

Learning System

This is where the magic happens. The app still uses a spaced repetition algorithm based on SuperMemo-2, but the way you interact with it changed a lot.

Mastery stages. Every item climbs through four stages, derived directly from its SRS state:

  • New - never reviewed
  • Learning - failed last time, or the interval is shorter than 3 days
  • Familiar - interval between 3 and 21 days
  • Mastered - interval of 21 days or more

The planner picks an exercise for each item based on its type and stage. The rule of thumb is "recognition first, recall later". A new Q&A item shows up as multiple choice (if there are enough other items to build good distractors). Once you've seen it, you have to type the answer. A cloze starts with a word bank and later makes you type the missing word. A diagram starts with labeling and later makes you find the spot yourself. Long answers that would be annoying to type fall back to a self-graded flashcard.

Mastery stages from new to mastered

Grading. Since most exercises are now objective, the app grades you instead of you grading yourself. Each outcome maps to an SM-2 grade:

  • Again (1) - wrong answer
  • Hard (2) - correct, but with help: a typo, extra guesses on a number, a mistake in a match, or at least 80% of a list
  • Good (3) - correct and clean
  • Easy (4) - only possible on self-graded flashcards

I made "Easy" flashcard-only on purpose. Getting a multiple choice question right doesn't mean you knew it easily, it might just mean the other options were bad.

Learn Sessions

A learn session has 12 exercises. The session builder picks due reviews first (most overdue first), then fills up with new items. Up to 10 new items fit in a session, and at least 2 are always included, even when many reviews are due, so you keep making progress instead of drowning in reviews. New items and reviews are mixed so a session never feels the same.

When you get something wrong, it comes back 3 to 5 exercises later in the same session, up to two times. But only the first attempt of an item writes to the SRS schedule. Retries, practice-ahead sessions (when nothing is due) and practice tests never change the schedule. This was one of those rules that sounds obvious once it's written down, but took a few iterations to get right.

Feedback for a correct and a wrong answer in a learn session

Right answers get a green "Nicely done!", wrong ones show the correct answer before you continue.

Study Modes

Besides learn sessions, every deck has three more modes that all share the same progress:

  • Flashcards - Classic flip cards with the 1-4 rating, for people who liked the old app just fine (hi).
  • Match - Tap matching tiles against the clock. Your best time is saved per deck.
  • Practice test - 20 recall-style questions with a score at the end. Doesn't change your schedule, so you can test yourself before an exam without messing anything up.

Match game

Gamification

I wanted the app to be motivating without being nagging. So there are no hearts, no lives, no ads and no levels (by design). What there is:

  • XP - 10 to 20 XP per correct exercise depending on how hard it is, a combo bonus for correct answers in a row (+2 for every 5, up to +10), half XP for correct retries, and a bonus for finishing a session (plus a little more for a perfect one). Wrong answers give nothing, but they don't take anything away either.
  • Daily goal - Pick a goal from casual to intense and fill your ring every day.
  • Streak with a safety net - One missed day is covered by a streak freeze. You can hold one freeze at a time and earn a used one back every 7 streak days. Days are counted in your own timezone, because a streak that breaks at midnight UTC when you live in Switzerland is just mean.
  • Achievements - 13 of them, from "First steps" to "Walking library" (master 250 items), plus a few for streaks, perfect sessions, a fast match game and a perfect practice test.

The XP values are validated on the server. The client sends what it thinks it earned, the server checks it against an upper limit. I don't expect anyone to cheat their way to a better streak on a study app, but you never know.

Gamification: XP, daily goal, streak and achievements

Progress Tracking

The app tracks various statistics including daily review counts, success rates, learning streaks, time-of-day analysis, and items by difficulty level. Since 2.0 there is also a daily_activity table that stores XP, exercises and time per user and day, which powers the streak, the daily goal and the activity heatmap.

I'll be honest - the statistics code was a nightmare for a while. It's much better now, but the statistics tab still reads from the review history while the new gamification reads from the daily activity. Two sources of truth is never a great idea, so that's on the list.

Subscription System

The Pro subscription system is unchanged from 1.11 and still works well:

  • Free Plan: All item types, exercises, study modes and gamification
  • Pro Plan: AI generation, advanced analytics, priority support
  • Billing Management: Stripe-powered checkout, billing portal, automatic payment recovery

The subscription gating is implemented throughout the app, with Pro features clearly marked and gracefully degraded for free users.

Technical Stack

Frontend

  • Next.js 16 - The main framework, with Turbopack. I really like how development feels with Next.js, even if it's probably overkill for some projects.
  • React 19.3 - Upgraded along the way, first because of the security patches, then as part of the 3.0 migration.
  • TypeScript 6 - In strict mode. The new item types are a discriminated union, which makes TypeScript actually useful instead of just annoying.
  • Tailwind CSS 4.3 - For styling.
  • Framer Motion - For flips, transitions and the little celebrations
  • shadcn/ui - Component library for consistent UI elements

Backend

  • Next.js Server Actions - Instead of building a separate API
  • Drizzle ORM - Turned out to be a solid choice, even if it caused a few hours of debugging frustration initially.
  • Zod 4 - Schemas for every item type, shared by the editor, the import and the AI
  • NextAuth.js - For authentication via magic email links

Database

  • Turso (libSQL) - SQLite-compatible database that scales
  • Drizzle migrations - For schema management

I chose Turso because it's basically SQLite but hosted, which gives you the simplicity of SQLite with the convenience of not having to manage your own database server.

External Services

  • Google Gemini via the AI SDK 7 - For AI generation, now Gemini 3 Flash
  • Stripe - Complete payment processing, subscriptions, and billing (SDK 23)
  • Resend - For sending authentication and payment-related emails
  • Upstash Redis - Rate limiting
  • Vercel - Hosting, cron jobs and Blob storage for images and PDF uploads

Testing & Tooling

  • Vitest 5 with Testing Library - 500+ tests, running in GitHub Actions on Node 24 on every push and PR
  • ESLint 10 - Flat config through the ESLint CLI, since next lint no longer exists
  • release-please - Versions and changelog from Conventional Commits

AI Integration

Current Implementation

The AI generator was more or less rebuilt for 2.0. It no longer creates only front/back cards, but mixed items of the types that fit the material: dates become number items, steps become sequences, definitions become Q&A, and so on.

AI generation from notes and PDFs

What Works:

  • Generates mixed item types from a text prompt or a PDF
  • Uses the deck's title, description and tags as context
  • You can pick roughly how many items you want (10, 20, 40, 60 or automatic)
  • Knows which items are already in the deck and skips repeats
  • Reads full PDFs up to 20 MB, including tables, figures and scanned pages

Technical Details:

  • Uses gemini-3-flash-preview by default, through the structured output of the AI SDK 7 (Output.object). The model is configurable via an environment variable, so the next model retirement is just a config change
  • The model returns a flat item object. Everything goes through the strict item schema, and items that fail validation are simply dropped
  • The prompts of the newest 200 items in the deck go to the model as context. New items whose normalized prompt matches an existing one are not saved
  • Without a requested count, an exact number from the prompt wins, otherwise the model picks between 10 and 60
  • Each run logs how many items survived each filter stage, which made debugging bad generations a lot easier
  • Model thinking is capped, and the request times out before Vercel does, so you get a proper error instead of a dead request

PDF uploads:

This one was more annoying than expected. Vercel Functions accept at most 4.5 MB per request, which is not a lot for lecture slides. So the browser now uploads the PDF straight to Vercel Blob using a presigned URL, and the server sends the full file to Gemini as a file part. After generation the blob is deleted, and a daily cron job cleans up anything older than one hour that was left behind. The server only accepts blob URLs in the user's own upload folder, so nobody can make the app read someone else's file.

Getting this to work with Vercel's OIDC authentication, the Content Security Policy and large streamed responses took a few rounds of fix(ai): commits. If you read the changelog for 2.0, you'll see exactly where I struggled.

Rate Limiting:

  • Free users: No AI access (encourages subscriptions)
  • Pro users: 50 generations per hour
  • Grace period users: 15 per hour (for payment issues)
  • Limited users: 5 per hour (suspended subscriptions)

Challenges & Solutions

Challenge: AI sometimes generated too many or too few cards Solution: An explicit count picker, plus a clear rule in the prompt for when no count is given

Challenge: The same questions every time you generate for a deck Solution: The existing prompts go to the model as context, and repeats are filtered out before saving

Challenge: Mixed item types are harder to get right than front/back cards Solution: A flat output format that is easy for the model, a type guide in the prompt, and strict validation afterwards

Challenge: PDF processing was unreliable and limited to 5 MB Solution: Direct upload to Blob and letting Gemini read the full PDF instead of extracting text myself

Challenge: The model got retired under my feet Solution: Model name in an environment variable. Fool me once...

Future AI Improvements

  • AI help while learning, not only while creating
  • Support for more file formats (DOCX, TXT, etc.)
  • Better handling of very long documents
  • Generating diagram items with images

Payment Integration

Stripe Implementation

The Stripe integration is fully implemented and handling real transactions:

What's Working:

  • Monthly and yearly Pro subscriptions
  • Secure checkout flow with automatic tax calculation
  • Billing portal for subscription management
  • Webhook handling for all subscription events
  • Automatic payment recovery system
  • Prorated plan changes

Technical Architecture:

  • Server actions for checkout session creation
  • Webhook endpoints for subscription events
  • Secure error handling that doesn't leak sensitive data
  • Rate limiting on payment endpoints
  • Customer data synchronization with local database

Payment Recovery: The payment recovery system is probably over-engineered, but it works:

  • Automatic retries for failed payments
  • Grace period with limited access
  • Email notifications at different stages
  • Automatic restoration when payment succeeds

Subscription Management

Users can:

  • Upgrade to Pro from any page
  • Manage billing through Stripe's customer portal
  • Change between monthly/yearly plans
  • Cancel subscriptions (with end-of-period access)
  • Update payment methods

The billing portal integration saves a ton of development time - Stripe handles all the complex billing scenarios.

Security Considerations

  • All webhook events are verified with Stripe signatures
  • Sensitive data is never logged or exposed in error messages
  • Rate limiting prevents abuse of payment endpoints
  • Payment data is handled entirely by Stripe

Revenue Model

The business model is straightforward:

  • Free plan covers everything you need to learn, including all new exercise types and gamification
  • Pro plan unlocks AI features and advanced analytics
  • Annual subscriptions get a significant discount

I deliberately kept all the new learning features from 2.0 free. AI costs real money per request, exercises don't.

Database Schema

Core Tables

The database schema has grown, but remains pretty straightforward:

Users & Auth (handled by NextAuth):

  • users - Basic user information
  • accounts, sessions, verificationTokens - Auth-related tables

Learning Content:

  • decks - Collections of items
  • flashcards - Items with type, JSON content, plus front (prompt) and back (plain-text answer summary)
  • card_reviews - Current SRS state of each item per user
  • review_events - Historical record of all reviews, now with the exercise kind that produced it

Gamification (new in 2.0):

  • user_stats - Total XP, current and longest streak, streak freezes, timezone
  • daily_activity - XP, exercises, correct answers and time per user and local date
  • user_achievements - Unlocked achievements (the definitions live in code)
  • deck_records - Personal bests for the match game and practice tests

Analytics & Settings:

  • study_sessions - Tracking learning sessions
  • user_preferences - Theme, language, animations, sound and daily goal

Business:

  • subscriptions - Stripe subscription data and status
  • payment_recovery_events - The payment recovery system

Yes, the table is still called flashcards even though it holds nine types of items now. Renaming a table in production for cosmetic reasons felt like asking for trouble.

Relationships

The relationships are pretty standard. Users have many decks, decks have many items, items have many review events, users have review history for items, and users have stats, daily activity and achievements.

Indexes & Performance

I've added indexes on the most commonly queried fields:

  • User ID (for obvious reasons)
  • Next review dates (for finding due items)
  • Deck associations
  • Review timestamps
  • Subscription status fields

The gamification tables use composite primary keys (user + date, user + achievement), so the common lookups are covered without extra indexes.

Migration Strategy

Database migrations are handled by Drizzle. When you want to add a field to a table, you extend the schema in code and generate new SQL migration files. Then you run the migration to safely apply changes to the actual database.

The 2.0 migration was the biggest one so far, but it was purely additive: new columns with defaults and new tables. Existing cards became basic items without any data being rewritten. The only clever bit is a backfill that builds the new daily_activity table from the existing review history, so people who had been learning for months didn't start 2.0 with an empty heatmap. All of that made me a lot less nervous about running it in production.

Authentication & Security

NextAuth.js Implementation

Authentication uses NextAuth.js with email-only login (no passwords). Users get a magic link sent to their email, click it, and they're logged in. It's simple and secure - no passwords to forget or leak.

Since 1.14, new users get a proper welcome email and returning users get a login email, instead of the same generic message for everyone.

Session Management

Sessions are JWT-based and handled entirely by NextAuth. The session includes the user ID, which is all we really need for most operations.

Rate Limiting

Rate limiting uses Upstash Redis and since 1.14 covers basically every critical feature:

  • Email sending (5 emails per 15 minutes)
  • AI generation (based on subscription tier)
  • Learning and study operations (with generous limits, intensive sessions shouldn't hit a wall)
  • Bulk operations and exports
  • Payment operations (strict limits)

The rate limiting saved me when I accidentally created an infinite loop during development that was sending hundreds of emails. It also keeps AI costs under control.

Security Headers

The app includes standard security headers like CSP (Content Security Policy), X-Frame-Options and HSTS (HTTP Strict Transport Security). The CSP is now built in one place and applied by the Next.js proxy (formerly middleware). Every new origin the browser talks to (like Vercel Blob for uploads) needs an explicit entry there, which I learned the hard way when the new PDF uploads were blocked right after release.

Data Protection

User data is isolated by user ID in all database queries. Server actions verify the user's session before performing any operations. Uploaded PDFs are only accepted from the user's own upload folder and deleted after generation.

Payment data is handled entirely by Stripe - we never store sensitive payment information locally.

Content Security

AI-generated content has to pass the same strict schema as everything else. There are also length limits per field, so a creative model can't sneak a novel into an answer.

Spaced Repetition System (SRS)

Algorithm Implementation

After some research (and prompting AI), I went with the SuperMemo-2 algorithm. I'm not skilled enough yet to implement SRS algorithms cleanly from scratch, so I had AI generate most of this initially (and it did a surprisingly good job).

The core algorithm didn't change in 2.0. What changed is everything around it: who decides the grade, which exercise you see, and when a review counts.

How It Works

When you review an item, the algorithm calculates:

  1. Ease Factor - How "easy" the item is (starts at 2.5)
  2. Interval - Days until next review (capped at 365 days)
  3. Next Review Date - When you'll see it again

The ease factor adjusts based on the grade:

  • Grade 1 (Again): Decreases ease and resets the interval
  • Grade 2 (Hard): Slightly decreases ease with a modest interval increase
  • Grade 3 (Good): Maintains ease with normal interval calculation
  • Grade 4 (Easy): Increases ease with a longer interval

For flashcards you still pick the grade yourself. For all other exercises, the grading logic picks it based on how you answered (see Learning System).

Review Scheduling

New items start with a 1-day interval. As you successfully review them, the intervals get longer: 1 day → 3 days → 8 days → 20 days → 48 days, etc.

The exact intervals depend on the ease factor and your performance on that specific item. The intervals also decide the mastery stage, so the exercises get harder exactly when the schedule says you know an item better.

Performance Metrics

The system tracks various metrics to help users understand their progress:

  • Items due for review
  • Items per mastery stage, per deck
  • Success rates by difficulty
  • Streaks, XP and daily activity
  • Time spent studying

Internationalization

Next-intl Setup

The app supports English and German using next-intl. The setup is pretty standard - you have message files for each language, and the library handles the rest.

Language Support

Currently supports:

  • English - Primary language
  • German - Secondary language (since I'm German and it was easy to add)

Adding more languages is just a matter of creating new translation files and updating the language selector.

Locale Management

User language preferences are stored in the database for authenticated users, and in cookies for guests. The locale is determined in this order:

  1. User's saved preference (if logged in)
  2. Cookie value
  3. Browser language detection
  4. Default to English

Translation Workflow

Translations are stored in JSON files under /messages/. When I need to add new text, I add it to the English file and then create the German translation.

2.0 roughly doubled the number of strings: every exercise, every achievement, the new landing page, docs, help and roadmap pages. There's now even a test that checks the typography in the translation files, because I got tired of fixing quotes and dashes by hand.

User Experience & Interface

Design System

The UI uses a consistent design system built on top of shadcn/ui components and Tailwind CSS.

For 2.0 the design got a lot more playful. The exercise components were ported from a design kit and have a clear Duolingo influence: big tappable tiles, a progress bar, a "Check" button, and green or red feedback at the bottom. The rest of the app stays clean and calm, so it doesn't feel like a slot machine.

Landing Page

The landing page was completely redesigned. The best part: every exercise kind is playable right on the page, using the real components from the app (nothing is saved). It's the easiest way to explain what the app does, and honestly also a great way to test the components.

There are also new docs, help, roadmap and contact pages, because "just ask me" doesn't scale well.

Responsive Design

The app works on mobile, tablet, and desktop. The exercises are built for touch first: big tiles, drag or arrow buttons for ordering, and tap targets for images.

Since 3.0, every page is checked at 320, 360, 375, 768 and 1280 px in English and German, with no horizontal scrolling anywhere. On small screens, some buttons and badges show only their icon so titles keep their space, and long German words hyphenate instead of breaking the layout.

Accessibility

Basic accessibility features are implemented:

  • Keyboard support in learn sessions (number keys for options, Enter to check and continue, Space to flip cards)
  • Proper ARIA labels and roles
  • Color contrast compliance
  • Arrow buttons as an alternative to dragging in "put in order"

There's definitely room for improvement here, but it covers the essentials.

Animations & Sounds

The app includes subtle animations powered by Framer Motion:

  • Flashcard flip transitions
  • Exercise feedback and session progress
  • Small celebrations for goals, streaks and achievements

There are now also short sound effects for correct and wrong answers. Both animations and sounds can be turned off in the preferences.

Theme Management

Supports light and dark themes, with system preference detection. Theme preference is stored in user settings and synced across devices.

Performance & Optimization

Bundle Optimization

Next.js handles most of the optimization automatically:

  • Automatic code splitting
  • Image optimization
  • Static generation where possible
  • Tree shaking

Database Queries

I've optimized the most common queries:

  • Proper indexing on frequently queried fields
  • Batching related data fetches

API Performance

  • AI generation streams progress to the client and times out cleanly
  • PDF uploads skip the server entirely
  • Stripe webhooks are processed efficiently with error recovery
  • Rate limiting prevents resource abuse

Core Web Vitals

The app generally scores well on Core Web Vitals, though there's always room for improvement. The main performance bottleneck is still the statistics dashboard when you have a lot of review history.

AI generation can take a while for large PDFs, but there are proper loading states and progress indication.

Known Issues & Limitations

Current Bugs

There are still some edge cases I haven't fully resolved:

  • The statistics tab and the new gamification read from different tables, so numbers can differ slightly
  • Very large or unusual PDFs can still fail or time out
  • Subscription status sometimes takes a moment to update after payment changes

Performance Bottlenecks

  • Statistics dashboard gets slow with large amounts of review history
  • AI generation for large documents is slow (but there's not much I can do about that)

Feature Limitations

  • No collaborative features (sharing decks, etc.)
  • No search across decks and items yet
  • No audio items (pronunciation would be great for language decks)
  • Limited file format support for AI generation (only PDFs)
  • Export is JSON only

Technical Debt

The good news: most of the technical debt from the initial proof of concept is gone. 2.0 moved the learning logic into pure, tested modules, and that forced a lot of cleanup.

The less good news:

  • The statistics code is the last big piece from the 1.x days
  • The database still says flashcards and card_reviews while the code says items
  • Some older components still use the classic card model and could use the new item types
  • Authentication is still on NextAuth v4, which also keeps nodemailer on an older major version. Moving to Auth.js v5 is overdue

Future Roadmap

Planned Features

Short Term:

  • Unify statistics and gamification data
  • Search and filters for decks and items
  • UI and UX polish on every screen size
  • Better keyboard shortcut discoverability

Medium Term:

  • More AI help while learning, not only while creating
  • Gamification improvements (without the pressure)
  • Collaborative deck sharing
  • More file formats for AI generation

Long Term:

  • Advanced SRS algorithms beyond SuperMemo-2 (FSRS looks interesting)
  • Audio items and pronunciation
  • Community features and a deck marketplace
  • Mobile apps (maybe)

Technical Improvements

  • Clean up the remaining statistics code
  • More end-to-end tests for the learn session
  • Better monitoring and error tracking
  • Performance optimizations for large decks

Scalability Considerations

The current architecture should handle several thousand users, but there are areas that would need attention for serious scale:

  • Database optimization for large review histories
  • Caching strategy improvements
  • Background job processing for AI features
  • AI cost optimization and batching

Contributing

Development Setup

You need Node.js 22.12 or newer and pnpm. To run the app locally:

Bash
1# Clone the repository 2git clone https://github.com/Fx64b/learn.git 3cd learn 4 5# Install dependencies 6pnpm install 7 8# Set up environment variables 9cp .env.local.example .env.local 10# Fill in your environment variables (you'll need Stripe, Google AI, etc.) 11 12# Run database migrations 13pnpm db:migrate 14 15# Start the development server 16pnpm dev

Code Contribution Guidelines

If you want to contribute, here are some guidelines:

  • Use TypeScript with proper types
  • Follow the existing code style and run pnpm format && pnpm lint
  • Add tests for new features (pnpm test)
  • Use Conventional Commits, release-please builds the changelog from them
  • Learning logic belongs in lib/learn/, item schemas in lib/items/
  • Be careful with subscription logic - test thoroughly
  • AI features should include proper error handling and rate limiting

Issue Reporting

If you find bugs or have feature requests, please open an issue on GitHub. Include as much detail as possible - it makes fixing things much easier. For subscription-related issues, please include your subscription status (but not payment details).

Feature Requests

I'm open to feature requests, though keep in mind this is primarily a personal project that happened to grow into a business. Features that align with the goal of "create a learning app that doesn't suck" are most likely to be implemented. There's also a feedback link on the new roadmap page.

License

This project is open source under the MIT License. Feel free to use it, modify it, or learn from it. If you do use it for something cool, I'd love to hear about it.

Note that while the code is open source, you'll need your own API keys for Google AI, Stripe, etc. to run the full feature set.

Activity

Alt

This documentation reflects the current state of what started as a simple study tool, somehow became a full SaaS application, and is now also a small learning game. The journey from "quick flashcard app" to "Duolingo meets Quizlet with spaced repetition" has been both educational and slightly overwhelming, but that's what makes side projects fun.

© 2026 Fx64bv5.2.0