ts-fsrs: a TypeScript toolkit for FSRS spaced repetition
ts-fsrs is a versatile package written in TypeScript that supports ES modules, CommonJS, and UMD.
At a glance
- What is it?
- ts-fsrs is an MIT licensed TypeScript package from the Open Spaced Repetition project that implements the FSRS scheduler for flashcard apps, paired with a second package that trains FSRS parameters from review logs. It suits a web or mobile app team that wants a modern spaced repetition scheduler in TypeScript, and it is a poor fit if you need a server side scheduler in Python or Go with no browser component.
- Who is it for?
- ts-fsrs suits a team building a flashcard application in TypeScript that wants the FSRS scheduler without implementing the algorithm, especially since the preview and review split lets you show all four possible outcomes before the user answers. It does not suit a backend that lives in another language, since the repository itself points to an awesome-fsrs list for the other implementations, and it does not suit a project that cannot move to Node 24.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Two packages with different jobs
The repository is a monorepo holding two published packages, and the README's table is the clearest description of the project.
The first is ts-fsrs, described as the scheduler for review flows. The second is @open-spaced-repetition/binding, described as the optimizer for parameter training and CSV conversion.
The split matters because the two are not at the same stage of the lifecycle. Scheduling is what happens every time a user opens the app and is due to review a card. Optimisation is what happens after you have accumulated enough review history to retrain the algorithm's parameters on your own data, which is the step that makes FSRS adapt to a particular learner rather than to the average of a large dataset.
Their supported FSRS versions differ, and that difference is not an accident. The scheduler badge lists FSRS 3, 4, 4.5, 5, 6 and 7, while the binding lists only FSRS 6 and 7. The older models are in the scheduler because you may be reading a review log that was written years ago, and the newer ones are in the binding because parameter training is a current concern.
The project is MIT licensed, the homepage is open-spaced-repetition.github.io/ts-fsrs, the last push to the repository was on 2026-10-19, and the recent releases include ts-fsrs 5.4.2 on 2026-09-01 and binding 0.5.0 on 2026-06-06.
The primary language is TypeScript, and the package description notes that the library supports ES modules, CommonJS and UMD, which matters if you are embedding it in something that is not a modern bundler.
The library version and the FSRS model version move separately
One sentence in the README covers a distinction that trips people up: use DefaultScheduler() to schedule reviews with ts-fsrs v6, and it defaults to FSRS-7, and the library version and the FSRS model version are independent.
That independence is the point of the design. The npm package version tracks the API of the library, so a major bump means the calling convention changed. The FSRS model version tracks the algorithm, so moving from FSRS 6 to FSRS 7 changes how stability and difficulty are computed, and the model can change without the package version changing at all.
For a team that stores review history, that separation is what lets you keep reading old logs. A card reviewed a year ago has a review log written under an older model, and the scheduler still has to interpret it. A scheduler that only spoke the newest model would either refuse those records or reinterpret them with the wrong rules.
The repository also ships a migration document, linked as v5 to v6 and FSRS-7 migration, which means the v5 to v6 step was a breaking change for the API and that FSRS-7 arrived alongside it. If you are upgrading from a v5 based codebase, that document is the one to read first, because the calling pattern changed and the default model changed with it.
preview() then review() is the pattern to design your UI around
The basic usage example in the README is short, and its shape tells you how to build the answer screen.
import { DefaultScheduler, Rating } from 'ts-fsrs'
const now = new Date('2026-01-01T00:00:00.000Z')
const scheduler = await DefaultScheduler()
const card = scheduler.newCard({ now })
// Preview all four possible outcomes before the user answers.
const preview = scheduler.preview({ card, now })
for (const { grade, card: previewCard, revlog } of preview) {
console.log(grade, previewCard, revlog)
}
// Apply the final rating after the user has already answered.
const result = scheduler.review({ card, grade: Rating.Good, now })
console.log(result.card)
console.log(result.revlog)There are two calls and they are separated on purpose. preview returns every possible outcome, one entry per grade, each with the card it would become and the review log entry that would be written. review applies the single grade the user actually chose.
That split is what lets an app show the four buttons with the intervals each one implies before the user commits, and then record only what was chosen. An app that called review directly would have to know the next intervals itself or recompute them after the fact.
Two smaller details are worth copying. The scheduler is created with await, so DefaultScheduler() is asynchronous even though it looks like a constructor, and every call takes an explicit now, which means your time source is yours rather than the library's, and your tests do not depend on the wall clock.
The Rating enum is what the user presses, with Rating.Good used in the example, and the review log entry is a first class output, so you should write it to your own storage rather than assuming the library persists it.
The optimizer package carries a Rust core
The binding package is the more involved half, and the repository layout explains why.
The root contains a Cargo.toml declaring a Cargo workspace whose single member is packages/binding, with a release profile that enables LTO, sets codegen-units to 1 and strips symbols. So the optimizer is not pure TypeScript. It has a Rust component, and the npm package wraps it.
That choice fits the workload. Parameter training is numerically intensive, running a gradient based optimisation over a review history, and Rust is a reasonable language for that inner loop. The TypeScript side is the interface, and the README's mention of browser and WASI setup for the binding package says that the Rust core is compiled to WebAssembly for the browser case.
The binding package also covers the chores around training: CSV conversion, timezone handling, and training examples, all documented in its own README. CSV conversion matters more than it sounds, because review logs usually arrive as an export from an existing app, and the timezones in those exports decide whether the trained parameters describe the schedule you intended.
Installation is a second command.
pnpm add @open-spaced-repetition/bindingAnd the two packages have to agree on the model version. The binding supports FSRS 6 and 7, so if your scheduler is running an older model you cannot train parameters for it with this package, and the README points to the model documentation pages for the supported versions on each side.
Node 24 is the floor for both packages
The installation section states one hard requirement: ts-fsrs v6 and @open-spaced-repetition/binding require Node.js >=24.0.0.
That is a recent baseline, and it is worth checking against your CI images and your end user devices before anything else. A library that needs Node 24 is a library that will not run in an older Electron build, in a corporate Node 20 image, or on a device with an old system Node, and the failure will arrive as a syntax or API error rather than as a friendly version check.
The package itself is installed with pnpm in the README, and the repository is a pnpm workspace with a pnpm-lock.yaml, so pnpm is the expected package manager for development. That is a choice with consequences for contributors, since npm and pnpm produce different node_modules layouts, and for consumers it is irrelevant, since the published package is the same either way.
The repository also has a .nvmrc file, which is how the local development environment pins its Node version, and a .devcontainer directory for a containerised setup, along with a CONTRIBUTING.md that covers how to set up a local development environment, how to set up a Dev Container, and the contribution guidelines. The .agents and .codex directories suggest the project is also set up for work with coding assistants.
For a library, the practical sequence is to check the Node floor, then read the quick start in the documentation, then look at the v5 to v6 and FSRS-7 migration guide if you are coming from an older version.
Turborepo, changesets, vitest and biome
The monorepo is set up with the usual modern tooling, and the details are visible in the root package.json and the file listing.
The root package is private and named ts-fsrs-monorepo, with scripts that delegate to turbo for check, typecheck, dev, test, test coverage, bench and build. Turbo is the task runner, and the note in the scripts explains a real constraint: use a single colon instead of a double colon in script names, because Turbo fails to parse glob expressions for cache log files when task names contain double colons.
Releases are managed with changesets. The changeset script creates a changeset, the version script runs changeset version, installs with a lockfile only update and pops the automated changesets stash, and the release script runs changeset publish. The note above it explains the other half of the process, namely that the lock file has to be updated with the new versions when the release pull request is created.
Tests run under vitest with an istanbul coverage provider, there is a separate turbo script for the CI variants of the test and coverage runs, and a bench script for benchmarks. Formatting and linting are handled by biome, with the biome.json config at the root. The documentation is built with rspress, and there are per filter scripts for the docs development server, build and preview, filtered to the @ts-fsrs/website package.
The examples directory has a nextjs project and a vite project, which tells you the two front end stacks the library is expected to work in, and the docs directory holds the guide content, including the quick start and the migration pages.
Editorial conclusion
ts-fsrs suits a team building a flashcard application in TypeScript that wants the FSRS scheduler without implementing the algorithm, especially since the preview and review split lets you show all four possible outcomes before the user answers. It does not suit a backend that lives in another language, since the repository itself points to an awesome-fsrs list for the other implementations, and it does not suit a project that cannot move to Node 24. Before you commit, check the FSRS model version you want against the supported list for the scheduler, which currently runs from FSRS 3 through FSRS 7, note that the library version and the model version move independently, and plan for the optimizer package, which has its own Node and Rust toolchain requirements and whose WASM setup is documented separately.
Frequently asked questions
What does ts-fsrs do?
ts-fsrs is a TypeScript toolkit for building spaced repetition systems with FSRS. It implements the Free Spaced Repetition Scheduler algorithm so developers can integrate FSRS into their flashcard applications, and the repository also publishes a companion package for training FSRS parameters from review logs.
How do I install ts-fsrs?
Run pnpm add ts-fsrs, and add pnpm add @open-spaced-repetition/binding if you also need parameter optimisation from review logs. Both ts-fsrs v6 and the binding package require Node.js 24.0.0 or later.
Which FSRS versions does ts-fsrs support?
The scheduler package lists FSRS 3, 4, 4.5, 5, 6 and 7 as supported, while the binding package for parameter training supports FSRS 6 and 7. The library version and the FSRS model version are independent of each other.
What is the difference between scheduler.preview() and scheduler.review() in ts-fsrs?
preview returns all four possible outcomes for a card with the card and review log each would produce, so the interface can show the intervals before the user answers. review applies the single grade the user chose and returns the resulting card and review log.
Is the ts-fsrs optimizer written in TypeScript?
The binding package includes a Rust component, since the repository root has a Cargo workspace whose only member is packages/binding. The binding README covers CSV conversion, timezone handling and browser or WASI setup.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/open-spaced-repetition-ts-fsrs)Was this useful?