How I Built a Spaced Repetition Plugin for Obsidian
I've been using Obsidian for almost everything; notes, project management, journaling but the one thing I couldn't do well inside it was spaced repetition. The existing flashcard plugins used SM-2 (Anki's old algorithm from 1987) or required cloud accounts. I wanted the latest scheduling science, local-first storage, and the ability to sync across devices without a subscription.
So I built Lemma: a flashcard plugin for Obsidian that uses the FSRS algorithm, stores everything in your markdown files, and optionally syncs via CouchDB. It's published in the Obsidian Community Plugins directory.

Architecture
Lemma is structured around a DataManager class that orchestrates three layers:
Markdown files → Card Parser → FSRS Scheduler → Storage (PouchDB/JSON)
↓
CouchDB Sync (optional)
All UI is built with Obsidian's native component system — ItemView, Modal, Setting — so the plugin looks and feels like a natural part of the editor.
Card Parsing
Cards live inside your notes. Any file with the #flashcards tag becomes a deck. Two formats are supported:
Basic cards use a simple front/back separator:
---card--- ^block-id
What is the capital of France?
---
Paris
Cloze deletion cards hide specific words within a sentence:
The capital of France is ==c1::Paris== and it has ==c2::2.1 million== people.
Each ==cN::text== produces a separate card. The front replaces the cloze with [...] and the back shows the full sentence. Cards from the same paragraph are grouped automatically.
The parser scans ---card--- blocks by splitting on that marker (case-insensitive), then splitting each block on \n---\n to separate front from back. Clozes are matched with ==c(\d+)::(.*?)== and iterated per paragraph.
Block IDs (^block-id) are optional but critical: without one, the card ID is a SHA-256 hash of the file path plus front content, so editing the front resets your progress. With a block ID, edits are safe. IDs are namespaced with the deck hash to prevent collisions across files.

FSRS Scheduling
Lemma uses ts-fsrs v4, a pure TypeScript implementation of the Free Spaced Repetition Scheduler. No Rust, no WASM — just math.
FSRS models each card with two parameters: stability (how long the memory lasts, in days) and difficulty (1–10 scale). When you rate a card, the algorithm updates both based on 17 configurable weights and computes the next interval:
next_interval = stability × scheduled_days
Cards transition through states: New → Learning → Review → Relearning. The repeat() call computes all four possible outcomes at once — the selected rating picks which one to apply.
Before you rate, you see the predicted intervals for each option on the buttons. This is computed by calling repeat() with the current card state and formatting the scheduled_days into human-readable ("4d", "2.1m", "1.3y").
Users can tune request retention (default 0.9), maximum interval (default ~100 years), and all 17 FSRS weights in the settings tab.
Dashboard

The dashboard is an ItemView in the right sidebar. Decks are grouped by folder (collapsible), and each shows due and new card counts. The header has:
- "Study all due" with a badge showing total due cards
- Statistics button
- Custom study button for filtered sessions
- Refresh, Help, and Sync buttons
- Stats pills — total cards, due, new
Each deck row has hover-revealed actions: Study (due cards), Cram (all cards), and Browse (read-only preview). Clicking the row opens the source note.
Folder grouping was added after users with large vaults found flat lists overwhelming. Decks are grouped by their directory path, sorted alphabetically, with expand/collapse animations.
Review Modal
When you start a study session, the plugin opens a full-screen modal:
- Front shown — Rendered with full Markdown (images, LaTeX, code blocks, embeds, callouts)
- Press Space/Enter — Answer slides in below a horizontal rule
- Rating buttons appear — Again (red), Hard (orange), Good (green), Easy (cyan) with interval hints
- Rate or use keyboard —
1/2/3/4
Cards render using Obsidian's MarkdownRenderer.render() with the source file path so relative links and embeds resolve correctly. Clicking the title bar shows a tooltip with FSRS debug data (stability, difficulty, reps, lapses). The edit button opens the source file directly.
Rating buttons collapse to a 2-column grid on narrow screens. Touch targets are 44px+ for mobile use.
Statistics

The statistics modal uses Chart.js v4.5.1 for two visualizations:
- 30-day activity — A filled line chart with smooth tension, themed accent color, no point markers (clean look), hover-revealed tooltips, day labels
- 7-day forecast — Orange bar chart with rounded corners predicting daily due counts
Four header stat cards sit above the charts: reviews today, due this week, mature cards (stability ≥ 21 days), and total learned.
Both charts use Obsidian's CSS variables for theming so they adapt to light/dark mode automatically. Charts are properly destroyed in onClose() to prevent memory leaks.
Storage: PouchDB and CouchDB Sync
By default, FSRS data and review history are stored in Obsidian's data.json. For collections exceeding ~10k cards, the plugin offers PouchDB (IndexedDB) with three document types: card states, review logs, and settings. Switching backends is a settings toggle with a one-click migration tool and integrity verification.
When sync is enabled, PouchDB starts continuous live replication with a CouchDB server:
The sync layer handles authentication (credentials injected into the URL, stripped from debug logs), retry with backoff (up to 5 attempts then stops), fatal error detection (401/403/404 stops immediately), and CORS error messaging. A "Test sync" button validates the connection end-to-end.
The sync URL is built from server URL, database name, and credentials — the buildAuthenticatedUrl() method in the settings tab handles URL parsing with proper percent-encoding for passwords containing special characters.
Custom Study Sessions

The custom study modal lets you filter by tags (comma-separated), card state (Due/New/Learning/All), and card limit. This is useful for exam prep — filter by a chapter tag, include all cards regardless of due status, and disable the daily limit for unlimited cramming.
Tag filtering checks both inline tags and frontmatter tags against the file's metadata cache.
Settings
The settings tab exposes everything you'd expect and a few things you wouldn't:
- Database — PouchDB toggle, migration tool
- Sync — CouchDB URL, database name, credentials, test button, status checker
- Review defaults — Max new cards per day (default 20), max reviews per day (default 200)
- Appearance — Font size slider (12–32px)
- FSRS parameters — Request retention, maximum interval, all 17 weights in a textarea, factory reset button
- About — Version, GitHub issues link, help modal
The full source is on GitHub at github.com/sapienskid/Lemma. It's ISC licensed, published in the Obsidian Community Plugins directory, and I'm actively maintaining it. If you've been looking for modern spaced repetition inside Obsidian, give it a try, have and idea sent pull request and open an issue if something breaks.