Back to projects

ankipack

Generate Anki .apkg decks with full FSRS support. Works in browsers, Node.js, and Bun.

MIT Updated Apr 19, 2026
Repository

ankipack

npm License: MIT

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.

Table of Contents

Features

  • Latest Anki format with V18 schema and protobuf-encoded configuration
  • Full FSRS support with desired retention, custom weights, and all scheduler options
  • 4 built-in note types: Basic, Basic (and reversed), Basic (type in the answer), Cloze
  • Custom note types with arbitrary fields, templates, and CSS
  • Media attachments for images, audio, and other files
  • Multiple decks in a single .apkg package
  • Preset isolation: generated presets never overwrite the user's existing Anki defaults
  • Tiny footprint: only 3 runtime dependencies (sql.js, fflate, @bufbuild/protobuf)
  • Cross-platform: runs anywhere JavaScript runs

Installation

# 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.

Quick Start

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);

Platform Support

ankipack works in any JavaScript environment. The only platform-specific part is how you initialize sql.js.

Node.js / Bun

import initSqlJs from "sql.js";
const SQL = await initSqlJs();

sql.js will automatically locate its WASM binary from node_modules.

Browser / Browser Extensions

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.

Download helper (browser)

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);

API

Package

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
MethodDescription
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)

Deck

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);
OptionTypeDefaultDescription
namestringrequiredDeck name. Use :: for subdecks
descriptionstringundefinedDescription shown in Anki's deck list (supports HTML)
configDeckConfigauto-generatedScheduler preset for this deck
idnumberautoCustom deck ID

DeckConfig

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.

Learning

OptionTypeDefaultDescription
learnStepsnumber[][1, 10]Learning steps in minutes
relearnStepsnumber[][10]Relearning steps for lapsed cards
graduatingIntervalGoodnumber1Days after graduating with Good
graduatingIntervalEasynumber4Days after graduating with Easy

Daily Limits

OptionTypeDefaultDescription
newPerDaynumber20Maximum new cards per day
reviewsPerDaynumber200Maximum reviews per day

Intervals

OptionTypeDefaultDescription
maximumReviewIntervalnumber36500Upper bound for intervals (days)
minimumLapseIntervalnumber1Minimum interval for lapsed cards (days)

FSRS

OptionTypeDefaultDescription
desiredRetentionnumber0.9Target recall probability (0 to 1)
fsrsParamsnumber[][]Custom FSRS model weights
historicalRetentionnumber0.9Historical retention for FSRS optimization
ignoreRevlogsBeforeDatestring""Ignore review logs before this date (YYYY-MM-DD)

Card Ordering

OptionTypeDefaultDescription
newCardInsertOrderstring"due""due" or "random"
newCardGatherPrioritystring"deck""deck", "deckThenRandom", "lowestPosition", "highestPosition", "randomNotes", "randomCards"
newCardSortOrderstring"template""template", "noSort", "templateThenRandom", "randomNoteThenTemplate", "randomCard"
reviewOrderstring"day""day", "dayThenDeck", "deckThenDay", "intervalsAscending", "intervalsDescending", "easeAscending", "easeDescending", "retrievabilityAscending", "retrievabilityDescending", "relativeOverdueness", "random", "added", "reverseAdded"
newMixstring"mixWithReviews""mixWithReviews", "afterReviews", "beforeReviews"
interdayLearningMixstring"mixWithReviews"Same as newMix

Burying

OptionTypeDefaultDescription
buryNewbooleanfalseBury new sibling cards until next day
buryReviewsbooleanfalseBury review sibling cards until next day
buryInterdayLearningbooleanfalseBury interday learning siblings

Leech

OptionTypeDefaultDescription
leechActionstring"tagOnly""suspend" or "tagOnly"
leechThresholdnumber8Lapses before flagging as leech

Timer / Audio

OptionTypeDefaultDescription
disableAutoplaybooleanfalseDisable automatic audio playback
capAnswerTimeToSecsnumber60Cap answer time recording
showTimerbooleanfalseShow timer on review screen
stopTimerOnAnswerbooleanfalseStop timer when answer is shown
secondsToShowQuestionnumber0Auto-advance: seconds on question (0 = off)
secondsToShowAnswernumber0Auto-advance: seconds on answer (0 = off)
waitForAudiobooleantrueWait for audio before showing answer button
skipQuestionWhenReplayingAnswerbooleanfalseSkip question audio on answer replay

SM-2 Fallback

These are only used when FSRS is not enabled.

OptionTypeDefault
initialEasenumber2.5
easyMultipliernumber1.3
hardMultipliernumber1.2
lapseMultipliernumber0.0
intervalMultipliernumber1.0

Easy Days

OptionTypeDefaultDescription
easyDaysPercentagesnumber[][]Per-weekday review load percentages

Model

A note type defining fields and card templates. Use the built-in presets or create custom ones.

Built-in Presets

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 }.

Custom Model

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}}',
    },
  ],
});

ModelOptions

OptionTypeDefaultDescription
namestringrequiredNote type name
fieldsFieldDef[]requiredField definitions
templatesTemplateDef[]requiredCard templates
typestring"normal""normal" or "cloze"
cssstringAnki defaultCSS applied to all cards of this type
sortFieldIndexnumber0Field index used for browser sorting
latexPrestringAnki defaultLaTeX preamble
latexPoststring\end{document}LaTeX postamble
latexSvgbooleanfalseRender LaTeX as SVG
idnumberautoCustom model ID

FieldDef

OptionTypeDefaultDescription
namestringrequiredField name (unique within the model)
stickybooleanfalseKeep value when adding new notes
rtlbooleanfalseRight-to-left text
fontNamestring"Arial"Editor font
fontSizenumber20Editor font size
descriptionstring""Placeholder text
plainTextbooleanfalseTreat as plain text (no HTML)

TemplateDef

OptionTypeDefaultDescription
namestringrequiredTemplate name
questionFormatstringrequiredQuestion side HTML (use {{FieldName}} for substitutions)
answerFormatstringrequiredAnswer side HTML (use {{FrontSide}} to include the question)
questionFormatBrowserstring""Alternative question template for browser view
answerFormatBrowserstring""Alternative answer template for browser view
browserFontNamestring""Browser column font
browserFontSizenumber0Browser column font size
targetDeckIdnumber0Override deck for this template's cards

Note

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);
OptionTypeDefaultDescription
modelModelrequiredNote type for this note
fieldsstring[]requiredField values (must match model's field count)
tagsstring[][]Tags for this note
guidstringautoCustom GUID (auto-generated base91 if omitted)

License

MIT License. See LICENSE for details.

Built with ❤️ by Oliver.

About ankipack

What is ankipack?

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.

What language is ankipack written in?

ankipack is primarily written in TypeScript (100% of the codebase). Other languages used include JavaScript (0%).

What license does ankipack use?

ankipack is released under the MIT license.

Most Similar to ankipack

Explore Something Different from ankipack