Generate Anki .apkg decks programmatically with full FSRS support.
Works in browsers (including extensions), Node.js, and Bun.
ankipack targets the latest Anki version (24.x+) and its modern schema (V18 with protobuf-encoded deck configs). As far as we know, this is the only JavaScript/TypeScript package that supports the latest Anki format, including FSRS scheduler settings baked directly into the generated deck.
.apkg package# bun
bun add ankipack sql.js
# npm
npm install ankipack sql.js
sql.js is a peer-like dependency that you initialize and pass to ankipack. This lets you control how the WASM binary is loaded, which is important in browsers and extensions.
import initSqlJs from "sql.js";
import { Package, Deck, DeckConfig, Model, Note } from "ankipack";
const SQL = await initSqlJs();
// Create a model (note type)
const model = Model.basic();
// Create a deck with FSRS settings
const deck = new Deck({
name: "My Vocabulary",
config: new DeckConfig({
name: "My Preset",
desiredRetention: 0.9,
newPerDay: 20,
}),
});
// Add notes
deck.addNote(new Note({ model, fields: ["bonjour", "hello"] }));
deck.addNote(new Note({ model, fields: ["merci", "thank you"] }));
// Export
const pkg = new Package();
pkg.addDeck(deck);
// Node.js / Bun: write to file
await pkg.writeToFile("vocab.apkg", SQL);
// Browser: get bytes for download
const bytes = await pkg.toUint8Array(SQL);
ankipack works in any JavaScript environment. The only platform-specific part is how you initialize sql.js.
import initSqlJs from "sql.js";
const SQL = await initSqlJs();
sql.js will automatically locate its WASM binary from node_modules.
import initSqlJs from "sql.js";
const SQL = await initSqlJs({
locateFile: (file) => `https://sql.js.org/dist/${file}`,
});
You can also bundle the WASM file locally and point locateFile to it. In browser extensions, you will typically include sql-wasm.wasm in your extension assets and reference it with chrome.runtime.getURL or a similar API.
const bytes = await pkg.toUint8Array(SQL);
const blob = new Blob([bytes], { type: "application/octet-stream" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "deck.apkg";
a.click();
URL.revokeObjectURL(url);
A container for decks and media files that produces the final .apkg.
const pkg = new Package();
pkg.addDeck(deck);
pkg.addMedia("photo.jpg", imageBytes);
await pkg.writeToFile("output.apkg", SQL); // Node.js / Bun
const bytes = await pkg.toUint8Array(SQL); // Browser
| Method | Description |
|---|---|
addDeck(deck) | Add a deck to the package |
addMedia(filename, data) | Attach a media file. Reference it in templates via its filename (e.g. <img src="photo.jpg">) |
toUint8Array(SQL) | Build the .apkg as a Uint8Array |
writeToFile(path, SQL) | Write the .apkg to disk (Node.js / Bun only) |
A named collection of notes with an associated scheduler preset.
const deck = new Deck({
name: "French::Vocabulary", // use :: for subdecks
description: "Chapter 1 words",
config: myConfig,
});
deck.addNote(note);
| Option | Type | Default | Description |
|---|---|---|---|
name | string | required | Deck name. Use :: for subdecks |
description | string | undefined | Description shown in Anki's deck list (supports HTML) |
config | DeckConfig | auto-generated | Scheduler preset for this deck |
id | number | auto | Custom deck ID |
Scheduler preset controlling how Anki schedules cards. Supports all FSRS settings.
const config = new DeckConfig({
name: "Cramming Preset",
desiredRetention: 0.85,
learnSteps: [1, 10],
newPerDay: 100,
maximumReviewInterval: 7,
buryNew: false,
});
Generated configs never use id=1, so they will not overwrite the user's existing default preset on import.
| Option | Type | Default | Description |
|---|---|---|---|
learnSteps | number[] | [1, 10] | Learning steps in minutes |
relearnSteps | number[] | [10] | Relearning steps for lapsed cards |
graduatingIntervalGood | number | 1 | Days after graduating with Good |
graduatingIntervalEasy | number | 4 | Days after graduating with Easy |
| Option | Type | Default | Description |
|---|---|---|---|
newPerDay | number | 20 | Maximum new cards per day |
reviewsPerDay | number | 200 | Maximum reviews per day |
| Option | Type | Default | Description |
|---|---|---|---|
maximumReviewInterval | number | 36500 | Upper bound for intervals (days) |
minimumLapseInterval | number | 1 | Minimum interval for lapsed cards (days) |
| Option | Type | Default | Description |
|---|---|---|---|
desiredRetention | number | 0.9 | Target recall probability (0 to 1) |
fsrsParams | number[] | [] | Custom FSRS model weights |
historicalRetention | number | 0.9 | Historical retention for FSRS optimization |
ignoreRevlogsBeforeDate | string | "" | Ignore review logs before this date (YYYY-MM-DD) |
| Option | Type | Default | Description |
|---|---|---|---|
newCardInsertOrder | string | "due" | "due" or "random" |
newCardGatherPriority | string | "deck" | "deck", "deckThenRandom", "lowestPosition", "highestPosition", "randomNotes", "randomCards" |
newCardSortOrder | string | "template" | "template", "noSort", "templateThenRandom", "randomNoteThenTemplate", "randomCard" |
reviewOrder | string | "day" | "day", "dayThenDeck", "deckThenDay", "intervalsAscending", "intervalsDescending", "easeAscending", "easeDescending", "retrievabilityAscending", "retrievabilityDescending", "relativeOverdueness", "random", "added", "reverseAdded" |
newMix | string | "mixWithReviews" | "mixWithReviews", "afterReviews", "beforeReviews" |
interdayLearningMix | string | "mixWithReviews" | Same as newMix |
| Option | Type | Default | Description |
|---|---|---|---|
buryNew | boolean | false | Bury new sibling cards until next day |
buryReviews | boolean | false | Bury review sibling cards until next day |
buryInterdayLearning | boolean | false | Bury interday learning siblings |
| Option | Type | Default | Description |
|---|---|---|---|
leechAction | string | "tagOnly" | "suspend" or "tagOnly" |
leechThreshold | number | 8 | Lapses before flagging as leech |
| Option | Type | Default | Description |
|---|---|---|---|
disableAutoplay | boolean | false | Disable automatic audio playback |
capAnswerTimeToSecs | number | 60 | Cap answer time recording |
showTimer | boolean | false | Show timer on review screen |
stopTimerOnAnswer | boolean | false | Stop timer when answer is shown |
secondsToShowQuestion | number | 0 | Auto-advance: seconds on question (0 = off) |
secondsToShowAnswer | number | 0 | Auto-advance: seconds on answer (0 = off) |
waitForAudio | boolean | true | Wait for audio before showing answer button |
skipQuestionWhenReplayingAnswer | boolean | false | Skip question audio on answer replay |
These are only used when FSRS is not enabled.
| Option | Type | Default |
|---|---|---|
initialEase | number | 2.5 |
easyMultiplier | number | 1.3 |
hardMultiplier | number | 1.2 |
lapseMultiplier | number | 0.0 |
intervalMultiplier | number | 1.0 |
| Option | Type | Default | Description |
|---|---|---|---|
easyDaysPercentages | number[] | [] | Per-weekday review load percentages |
A note type defining fields and card templates. Use the built-in presets or create custom ones.
Model.basic() // Front/Back, 1 card per note
Model.basicAndReversed() // Front/Back + reversed, 2 cards per note
Model.basicTyping() // Front/Back with type-in answer
Model.cloze() // Cloze deletions ({{c1::text}})
All presets accept optional { name?: string, css?: string }.
const model = new Model({
name: "Vocab (type answer)",
css: `.card { font-size: 24px; text-align: center; }`,
fields: [
{ name: "Question" },
{ name: "Answer" },
{ name: "Notes", description: "Optional extra context" },
],
templates: [
{
name: "Card 1",
questionFormat: "{{Question}}\n\n{{type:Answer}}",
answerFormat: '{{Question}}<hr id="answer">{{type:Answer}}<br>{{Notes}}',
},
],
});
| Option | Type | Default | Description |
|---|---|---|---|
name | string | required | Note type name |
fields | FieldDef[] | required | Field definitions |
templates | TemplateDef[] | required | Card templates |
type | string | "normal" | "normal" or "cloze" |
css | string | Anki default | CSS applied to all cards of this type |
sortFieldIndex | number | 0 | Field index used for browser sorting |
latexPre | string | Anki default | LaTeX preamble |
latexPost | string | \end{document} | LaTeX postamble |
latexSvg | boolean | false | Render LaTeX as SVG |
id | number | auto | Custom model ID |
| Option | Type | Default | Description |
|---|---|---|---|
name | string | required | Field name (unique within the model) |
sticky | boolean | false | Keep value when adding new notes |
rtl | boolean | false | Right-to-left text |
fontName | string | "Arial" | Editor font |
fontSize | number | 20 | Editor font size |
description | string | "" | Placeholder text |
plainText | boolean | false | Treat as plain text (no HTML) |
| Option | Type | Default | Description |
|---|---|---|---|
name | string | required | Template name |
questionFormat | string | required | Question side HTML (use {{FieldName}} for substitutions) |
answerFormat | string | required | Answer side HTML (use {{FrontSide}} to include the question) |
questionFormatBrowser | string | "" | Alternative question template for browser view |
answerFormatBrowser | string | "" | Alternative answer template for browser view |
browserFontName | string | "" | Browser column font |
browserFontSize | number | 0 | Browser column font size |
targetDeckId | number | 0 | Override deck for this template's cards |
A single note containing field values. Generates one or more cards based on its model.
const note = new Note({
model: Model.basic(),
fields: ["What is 2+2?", "4"],
tags: ["math", "easy"],
});
deck.addNote(note);
| Option | Type | Default | Description |
|---|---|---|---|
model | Model | required | Note type for this note |
fields | string[] | required | Field values (must match model's field count) |
tags | string[] | [] | Tags for this note |
guid | string | auto | Custom GUID (auto-generated base91 if omitted) |
MIT License. See LICENSE for details.
Built with ❤️ by Oliver.
ankipack is Generate Anki .apkg decks with full FSRS support. Works in browsers, Node.js, and Bun.. It is built with TypeScript, JavaScript and maintained by Oliver Seifert.
ankipack is primarily written in TypeScript (100% of the codebase). Other languages used include JavaScript (0%).
ankipack is released under the MIT license.