mnemonic

spaced repetition in the browser, fsrs-5, no account

flashcards scheduled by fsrs-5. decks, cards, review history, and stats live in indexeddb, so there is no server and no network dependency after first load. sync to a shoal server is optional.

react 19, react router 7, typescript, vite, tailwind 4, dexie, ts-fsrs, shoal-client

a flashcard with the answer revealed and four rating buttons showing the next interval for each
study: the interval each rating would produce is shown before committing
the deck list with due counts
decks, with the sample data loaded
the stats page with totals and a 52-week heatmap
stats

Mnemonic is a spaced-repetition flashcard app that runs entirely in the browser. Scheduling is FSRS-5 through ts-fsrs. Persistence is Dexie over IndexedDB. The UI is React 19 with React Router 7 and Tailwind 4 on the ash lumen design system.

architecture

where does a rating go, and what reads it back?

card rows are updated in place by FSRS. every rating also appends an immutable review log row, which is what the stats read.

stored state
IndexedDB via Dexie decks, cards with FSRS state reviewLogs Study page space flips, 1-4 rates ts-fsrs, FSRS-5 retention 0.9 Stats + heatmap 52 weeks, local dates shoal outbox optional sync useLiveQuery, due <= now rating update card, append log reviewLogs deck/ card/ review/

logs store a local YYYY-MM-DD string instead of a UTC timestamp, so a late-night review lands in the right heatmap square. there is no separate new-card queue: due <= now is the whole session query.

Three tables. Decks are name and creation time. Cards store the raw FSRS state inline: stability, difficulty, elapsed and scheduled days, reps, lapses, learning steps, state, due time, and last review. Review logs are one immutable row per rating with the card, the deck, the rating, a timestamp, and a local calendar date. Schema version 3 adds the three sync tables. Every version bump has been additive.

One fsrs() instance is configured with a 0.9 retention target, a 365-day maximum interval, and fuzz enabled. The study page runs a single Dexie query per session, cards in the deck with due <= now sorted by due, and snapshots the result into component state. Learning steps are whatever ts-fsrs returns, and re-due cards surface in the next session. All four ratings are previewed before the user commits, so each button shows its resulting interval.

Reads are live queries through dexie-react-hooks, so there is no state store. Sync is opt-in: records deck/, card/ and review/ go to an outbox with tombstones for deletes, drained five seconds after a write burst and on tab focus.

technical decisions

  • FSRS-5 over SM-2. FSRS models stability and difficulty separately instead of one per-card multiplier.
  • Local-first with no backend. No account, offline after first load, and the app is unchanged when the sync server is down or unconfigured.
  • Dexie over raw IndexedDB. Typed tables, versioned migrations, and live queries.
  • A review log separate from card state. FSRS mutates the card row in place. Every rating also appends a log row, and the heatmap and totals read the log, so history survives rescheduling.
  • Local day boundaries. Logs store a local YYYY-MM-DD string rather than a UTC timestamp, so a late-night review lands in the right heatmap square. The heatmap grid is Monday-anchored.
  • Keyboard flow. Space flips, 1 to 4 rates.

hard problems

  • Rating.Manual is zero and is excluded from ts-fsrs’s preview type, so indexing the scheduling result needs a Grade cast.
  • scheduled_days is zero for cards in the learning phase, so the displayed interval is computed from the due time minus now.
  • Deleting a deck under sync tombstones each card individually, so other devices cascade the same way.

numbers

measurevalue
sourceabout 1,700 lines across 19 files
testsnone. CI gates on lint and tsc -b
heatmap52 weeks
schedulerretention 0.9, maximum interval 365 days