Known has to mean something
The JLPT runs five levels, N5 for beginners up to N1 near native, and each one carries a rough vocabulary target. Most apps that help you get there fall into two camps. Flashcard tools drill words but never tell you when a word has actually stuck, and exam-prep tools quiz you without keeping any long-range model of what you know. I wanted the space in between: a platform that treats a word you know as a piece of data, learning speed as a number, and level readiness as something you can put a date on.
That framing set the constraints. The system is multi-tenant from the first commit, since more than one learner keeps a deck on it. Long-term study history is worth protecting, so account access sits behind a TOTP second factor with its own enrollment and backup-code flow. And the spacing logic lives in the database rather than the browser, so a learner who reviews on a laptop at night and a phone the next morning sees one consistent schedule.
One ladder, three decks
Vocabulary is not the only thing a JLPT learner has to hold in memory. Kana and kanji are separate decks with their own review histories. Rather than write three review engines, I gave all three the same Leitner stepper, so a word, a character, and a syllable move through review by identical rules.
A Leitner system sorts items into numbered boxes. A freshly seen item sits low, and every box maps to a fixed wait before the item is due again. The ladder here is six boxes deep, with waits of 0, 1, 3, 7, 16, and 35 days. An item in box 0 comes back the same day. One that has climbed to box 5 is not due again for over a month. Because the interval depends only on the box number, the schedule is settled by where an item currently sits, and nothing has to carry a hand-tuned date of its own.
Promote, demote, or wait
The stepper does one job. It takes the item’s current box and whether the learner just got it right, then returns the new box and the next review date. A correct answer promotes the item one box, capped at the top of the ladder. A wrong answer knocks it down one box, floored at zero, so a word you keep missing keeps returning on the short intervals until it holds. Once an item reaches box two it counts as known, which is the signal the rest of the app reads when it asks how much of a level you have mastered.
// One stepper drives every deck (word, kanji, kana). Correct moves the item up
// a box, wrong moves it down one, and the box index alone picks the next review
// date off a fixed ladder, so nothing per-item needs a schedule of its own.
const INTERVALS = [0, 1, 3, 7, 16, 35]; // wait in days at each box, 0..5
const KNOWN_AT = 2; // box at which an item counts as "known"
export function stepSrs(box: number, correct: boolean) {
const next = correct
? Math.min(box + 1, INTERVALS.length - 1) // promote, capped at the top box
: Math.max(box - 1, 0); // demote, floored at box 0
return {
srsLevel: next,
status: next >= KNOWN_AT ? "known" : "learning",
nextReview: new Date(Date.now() + INTERVALS[next] * 86_400_000),
};
} Sharing this one function is what keeps the three decks honest with each other. A change to the interval ladder, or to where the known line sits, lands on vocabulary, kanji, and kana in the same commit, so none of them can quietly drift onto a different schedule.
Two clocks on the same deck
The dashboard shows two kinds of progress, and they run off different math on purpose.
The first is mastery, and it gates the levels. For each level the app scores every category that has content, vocabulary and kanji, as the share of that level’s deck marked known. It averages those category scores, and a level counts as passed once the average clears ninety percent. N5 is open from the start, and each higher level unlocks only after the one below it passes. Empty decks are skipped rather than scored as zero, so a level with no kanji loaded yet is not held hostage by content that does not exist.
The second is the estimate, and it answers a different question: when will you get there. That one runs off cumulative word targets per level, roughly 800 words by N5 up through 10,000 by N1. These figures are the widely quoted approximations, not an official list, since the JLPT does not publish an exact vocabulary. The prediction takes how many words you know, divides the remaining count by your recent words-per-day pace, and turns that into a date for each level. The velocity feeding it comes from the same review history, alongside the streak of consecutive days practiced.
Keeping the two apart is the point. Mastery decides what you are allowed to study next, so it has to be strict and deck-based. The ETA is a motivator, so it can lean on rough targets and a projected pace without either one leaking into the gate.
A level the server won’t unlock
Level gating is the kind of rule a client will happily break. If the only thing stopping a learner from grinding N1 words on day one is a disabled button, the button is not really a gate. So the same level computation runs on the server, and the review endpoint recomputes a learner’s unlocked levels from their own deck before accepting an answer. A review aimed at a level that is not unlocked gets refused there, not merely hidden in the UI. The client renders the state; the decision about whether a level is reachable is made where the data lives.
Modes, and a build of one
Review is not only reading a word off a card. The same deck feeds a listening mode, where the prompt plays as synthesized audio, and a speaking mode, where in-browser speech recognition scores what the learner says against the expected reading. An AI practice mode calls a language model to write example sentences, explain a nuance, and quiz usage in context rather than by rote. Those model and audio calls run server-side in streaming mode where the provider supports it, so a slow network shows partial output arriving instead of a spinner, which is the difference between a review loop that keeps its rhythm and one that stalls.
This was a solo R&D build, so the architecture, data model, API, review engine, and interface are all mine. The part I would not sign off on faith was the scheduling and prediction math, because a miscalibrated ladder either buries a learner or hands out mastery too cheap. I walked the streak and pace counters through the cases that usually break them, a first day of activity, a gap of several days, and a session with nothing but wrong answers, before I let the dashboard present a number as true.
Outcomes
- 6-box SRS
- Velocity + ETA
- 4 practice modes
- Full-stack solo
- 2FA + multi-tenant
- A date, not a bar