OpenDeck v3 Explainer
OpenDeck v3 Explainer
OpenDeck v3 is a universal flashcard deck package format. It is designed for three equal priorities:
- Ease of use: a small deck should be easy to create and inspect.
- App integration: a study app should be able to load published decks without implementing authoring logic.
- Portability: a deck should travel as a folder or a plain
.ziparchive without hidden local paths, opaque databases, or deck-provided code.
The short version:
deck.json
records/
sources.jsonl
assets.jsonl
notes.jsonl
cards.jsonl
runtime/
cards.jsonl
media/
images, audio, video, fonts, datasets, and other local asset files
presentation.json
capabilities.json
authoring/
The important split is:
records/is the canonical editable source.runtime/cards.jsonlis the resolved app-facing study data.- Local media files are ordinary package files, usually under
media/, and are indexed byrecords/assets.jsonl.
Published decks must include runtime cards and every local asset referenced by those runtime cards. Simple apps can ignore notes, generators, field references, extraction logs, and authoring files.
Why Not Just JSON, CSV, Or APKG?
CSV is excellent for front,back, but it collapses when a deck has images, audio, multiple cards per note, cloze generation, source provenance, stable IDs, typed answers, or widget fallbacks.
One large JSON file is easy to parse, but bad for review and authoring diffs. JSONL keeps one record per line, which works well with Git, streaming import, and partial validation.
APKG is common because Anki is common, but OpenDeck is not trying to clone Anki. In particular, OpenDeck rejects arbitrary template HTML/CSS/JavaScript as the portability model. Imported APKG concepts should become typed records, declared capabilities, and static fallbacks where possible.
The Core Model
OpenDeck separates the deck into five kinds of data:
| Layer | File | Purpose |
|---|---|---|
| Package metadata | deck.json | Identity, schema version, package profile, minimum renderer profile, optional counts, entrypoints |
| Canonical source | records/*.jsonl | Notes, cards, assets, sources |
| Local binary assets | media/ or another package-relative path | Images, audio, video, fonts, datasets, and other files indexed by records/assets.jsonl |
| Runtime output | runtime/cards.jsonl | Fully resolved cards for study apps |
| Optional support | presentation.json, capabilities.json, authoring/ | Display hints, optional features, working material |
This separation keeps rich authoring possible without making every app implement the authoring system.
deck.json
deck.json answers the first questions an importer has:
- What schema is this?
- What deck is this?
- Is it source-only or published?
- What renderer profile does it need?
- Where are the records?
Example:
{
"schema": "opendeck.v3",
"id": "rust-book-grammar",
"revision": "2026-05-30.1",
"title": "Rust Book Grammar Points",
"languages": ["en"],
"license": "MIT OR Apache-2.0",
"profiles": {
"package": "published",
"minimumRenderer": "static-renderer.v1"
},
"counts": {
"sources": 1,
"notes": 995,
"cards": 995,
"runtimeCards": 995
},
"entrypoints": {
"sources": "records/sources.jsonl",
"notes": "records/notes.jsonl",
"cards": "records/cards.jsonl",
"runtimeCards": "runtime/cards.jsonl"
}
}
Reasoning:
schemalets validators reject unsupported packages.idis stable across revisions, so apps can associate progress with the same deck over time.revisionchanges when deck content changes.languagesis a list because decks are often bilingual or multilingual.profiles.packagetells an app whether it can study the package directly.profiles.minimumRenderertells the app the lowest renderer profile that can present a reviewable version of the deck.entrypointsshould only list files or directories actually included in the package.countsis optional validation metadata; if present, it must match the real record counts.
Package Profiles
OpenDeck has two package profiles:
source: editable package. It must include canonical notes and cards, but may omit runtime cards.published: app-importable package. It must includeruntime/cards.jsonland every local asset referenced by runtime cards.
Reasoning:
Authoring and studying have different needs. A source package can keep compact field references and working material. A published package must be ready for apps.
Renderer Profiles
OpenDeck has two baseline renderer profiles:
static-renderer.v1: text, markdown, code, media, groups, links, math, tables, occlusion fallback, widget fallback, and self-rating.interactive-renderer.v1: static rendering plus supported widgets and non-self-rating answer modes.
Reasoning:
The universal baseline is static review. Interactivity is allowed, but it must be declared and degradable when possible.
Notes
Notes are reusable knowledge records. A note is not necessarily one card.
Example:
{
"id": "appendices-gp-0001",
"kind": "rust.grammar-point",
"title": "Can Rust keywords be used as ordinary identifiers?",
"sourceRefs": [
{
"sourceId": "rust-book",
"path": "appendix-01-keywords.md",
"heading": "Keywords Currently in Use",
"lines": [1, 20]
}
],
"fields": {
"prompt": [
{"kind": "markdown", "text": "Can Rust keywords be used as ordinary identifiers?"}
],
"rule": [
{"kind": "markdown", "text": "Reserved Rust keywords cannot normally be used as identifiers."}
],
"invalidExample": [
{"kind": "code", "language": "rust", "text": "fn match() {}"}
]
}
}
Reasoning:
- Notes avoid duplicated source data.
- Fields are typed block arrays, not raw HTML strings.
- Source references live with the knowledge record.
kindis descriptive. Renderers should not need custom logic for every note kind.
Cards
Cards are review tasks. Canonical cards may use fieldRef to refer to note fields.
Example:
{
"id": "appendices-gp-0001/recall",
"noteId": "appendices-gp-0001",
"deckPath": ["Rust Book"],
"kind": "recall",
"front": [
{"kind": "fieldRef", "field": "prompt"}
],
"back": [
{"kind": "fieldRef", "field": "rule"},
{"kind": "fieldRef", "field": "invalidExample"}
],
"answer": {
"mode": "self-rating"
},
"fingerprint": "sha256:<64 lowercase hex chars>"
}
Reasoning:
- Stable card IDs let progress survive edits.
fingerprintlets apps notice review-relevant content changes. Real fingerprint values use the formsha256:<64 lowercase hex chars>.deckPathmodels subdecks without Anki-style delimiter strings.fieldRefkeeps canonical cards compact and editable.fieldRefis forbidden in runtime cards.
Runtime Cards
Runtime cards are fully resolved cards for study apps.
Example:
{
"id": "appendices-gp-0001/recall",
"noteId": "appendices-gp-0001",
"deckPath": ["Rust Book"],
"kind": "recall",
"front": [
{"kind": "markdown", "text": "Can Rust keywords be used as ordinary identifiers?"}
],
"back": [
{"kind": "markdown", "text": "Reserved Rust keywords cannot normally be used as identifiers."},
{"kind": "code", "language": "rust", "text": "fn match() {}"}
],
"answer": {
"mode": "self-rating"
},
"fingerprint": "sha256:<64 lowercase hex chars>"
}
Reasoning:
This is the app integration contract. A simple app should not need to resolve note fields, run generators, inspect source refs, or evaluate note-field conditionals. It can read resolved front/back blocks and render them.
Runtime rules:
- no
fieldRef; - no
when.fieldPresentorwhen.fieldEmpty; - no dependency on
records/notes.jsonl; - card order matches canonical order where possible;
- if no explicit non-negative integer
orderexists, JSONL line order is the default import/new-card order.
Blocks
Blocks are the portable rendering unit. Core block kinds include:
textmarkdowncodeimageaudiovideomathtablelinkgroupocclusionwidgetlegacyHtmlfieldReffor canonical cards only
Example image block:
{"kind":"image","assetId":"art.europe-after-rain","alt":"Europe After the Rain II by Max Ernst"}
Reasoning:
Typed blocks are boring but powerful. They let apps render unknown subject domains without executing deck code. They also give validators something concrete to check.
Markdown blocks use portable CommonMark with raw HTML disabled; media should use typed media blocks. Link blocks use safe schemes such as http:, https:, mailto:, or package-relative paths. Renderers reject executable schemes such as javascript:, data:, and file:.
legacyHtml exists for imports, but it is not normal authoring. It must include fallback content. Renderers sanitize it, do not execute scripts or event handlers, and may ignore it entirely.
Assets
Assets are referenced by ID, not by path.
Example:
{
"id": "art.europe-after-rain",
"path": "media/2014-08-19_020719.jpg",
"mime": "image/jpeg",
"sha256": "sha256:<64 lowercase hex chars>",
"bytes": 123176,
"alt": "Europe After the Rain II by Max Ernst",
"attribution": [
{
"label": "WikiArt",
"url": "https://www.wikiart.org/en/max-ernst/europe-after-the-rain-ii"
}
]
}
Reasoning:
- IDs are stable even if files are reorganized.
- Hashes let apps verify and cache assets.
- Real
sha256values use the formsha256:<64 lowercase hex chars>. - MIME and byte size help importers and renderers.
- Attribution travels with the media.
- Published local assets must include
path,mime,sha256, andbytes.
Sources
Source records preserve provenance for source-derived decks.
Example:
{
"id": "rust-book",
"kind": "git-repo",
"title": "The Rust Programming Language",
"url": "https://github.com/rust-lang/book",
"commit": "05d114287b7d6f6c9253d5242540f00fbd6172ab",
"pathPrefix": "src",
"license": "MIT OR Apache-2.0"
}
Reasoning:
Source references matter for generated educational decks. They let reviewers trace cards back to source material without making source files part of the runtime contract.
Generated Cards
OpenDeck does not require renderers to generate cards at study time.
Generated card types include:
- reverse cards;
- cloze cards;
- image occlusion cards;
- artwork artist/title cards;
- source-derived grammar cards.
The generated cards must appear as concrete card records.
Example cloze runtime card:
{
"id": "rust.ownership.borrowing/c1",
"noteId": "rust.ownership.borrowing",
"deckPath": ["Rust Book", "Ownership"],
"kind": "cloze",
"front": [
{"kind": "markdown", "text": "You can have either one mutable reference or any number of [...] references."}
],
"back": [
{"kind": "markdown", "text": "You can have either one mutable reference or any number of immutable references."}
],
"answer": {
"mode": "self-rating"
},
"origin": {
"generator": "cloze.v1",
"sourceField": "body",
"group": "c1"
},
"fingerprint": "sha256:<64 lowercase hex chars>"
}
Reasoning:
Every app should be able to study a published deck without implementing every possible card generator.
Capabilities And Widgets
Capabilities declare required behavior, optional behavior, dependency files, and fallbacks.
Example:
{
"requires": [],
"optional": [
{
"id": "widget.stroke-order.v1",
"fallback": "static"
}
],
"dependencies": [
{
"id": "hanzi-writer-data",
"kind": "dataset",
"requiredBy": ["widget.stroke-order.v1"],
"remote": {
"url": "https://example.org/datasets/hanzi-writer-data-2.0.0.zip",
"sha256": "sha256:<64 lowercase hex chars>",
"bytes": 12345678
},
"localAssetId": "dataset.hanzi-writer-data",
"offlineRequired": false
}
]
}
Reasoning:
Some decks need interactivity, but arbitrary deck-provided JavaScript is not portable or safe. OpenDeck allows known capabilities with declared dependencies and fallback blocks.
If an app does not support an optional widget, it renders the fallback.
Required capabilities have id and optional reason; if unsupported, the app rejects the deck. Remote dependencies should be version-pinned and include integrity metadata when they resolve to a file. Offline published decks vendor required dependencies as local assets.
Answer Modes
OpenDeck supports review input modes:
self-ratingtypedchoicedraw
Example typed answer:
{
"mode": "typed",
"expected": ["cargo build"],
"normalize": "trim",
"fallback": "self-rating"
}
Reasoning:
The universal scheduler interface is still a rating. Typed and choice answers can help decide a rating, but progress belongs to the app. Static renderers can degrade to self-rating only when the answer mode declares that fallback.
Presentation
presentation.json is advisory. It can suggest density, fonts, code theme, and simple layouts.
Reasoning:
Presentation is allowed, but it must not be executable. Apps may ignore it and still render the deck. Layout tokens such as stack, row, grid, split, and media-first are advisory; unknown tokens are ignored.
Authoring Material
authoring/ is for working files:
authoring/
rust-book/
extraction-runs/
rejected-candidates.jsonl
chapter-markdown/
Reasoning:
LLM extraction logs, rejected candidates, source snapshots, and drafts are useful, but they are not runtime deck data. Keeping them separate lets a package be transparent without making study apps parse authoring history.
Packaging
An OpenDeck package can be:
- an unpacked folder;
- a plain
.ziparchive of the same folder contents.
Reasoning:
Folders are good for Git and authoring. ZIP archives are good for import/export and mobile sharing. The content model is the same either way.
Rules:
deck.jsonis at the package root.- Paths are package-relative.
- Paths must not escape the package root.
- Published decks include all local runtime assets.
- Publishing tools should produce deterministic ZIPs with normalized
/separators, stable entry ordering, and no absolute paths.
Validation
A validator should fail published decks for structural problems such as:
- missing
deck.json; - unsupported schema;
- invalid JSONL;
- duplicate IDs;
- missing notes, fields, or assets;
- runtime
fieldRef; - unresolved runtime note-field conditionals;
- unsafe Markdown or link schemes;
- missing published asset integrity metadata;
- unsupported required capabilities;
- missing fallbacks.
Reasoning:
Validation is what makes the format app-friendly. A study app should be able to reject a broken package before rendering.
Progress Is Outside The Deck
User progress does not belong in the package.
An app progress record can reference deck and card identity:
{
"deckId": "rust-book-grammar",
"deckRevisionSeen": "2026-05-30.1",
"cardId": "appendices-gp-0001/recall",
"cardFingerprintSeen": "sha256:<64 lowercase hex chars>",
"scheduler": "fsrs",
"state": {}
}
Reasoning:
Decks should be shareable content. Review logs, FSRS state, suspended cards, buried cards, and user settings are personal app data.
Importing APKG Concepts
OpenDeck can import APKG concepts, but it is not lossless APKG.
Mapping:
- Anki notes become OpenDeck notes.
- Anki generated cards become concrete OpenDeck cards.
- Anki subdecks become
deckPath. - Media becomes asset records.
- Simple field substitutions become canonical
fieldRef. - Conditional sections become canonical
when. - HTML-only content may become
legacyHtmlwith fallback. - JavaScript cards become known widgets only when a safe capability exists.
- Scheduling state stays outside the deck.
Reasoning:
The import goal is useful migration, not reproducing Anki's execution model.
Minimum Deck Example
A tiny published deck can be very small:
deck.json
records/notes.jsonl
records/cards.jsonl
runtime/cards.jsonl
deck.json can be only the entrypoints it actually includes:
{
"schema": "opendeck.v3",
"id": "basic-rust-commands",
"revision": "2026-05-30.1",
"title": "Basic Rust Commands",
"languages": ["en"],
"profiles": {
"package": "published",
"minimumRenderer": "static-renderer.v1"
},
"counts": {
"notes": 1,
"cards": 1,
"runtimeCards": 1
},
"entrypoints": {
"notes": "records/notes.jsonl",
"cards": "records/cards.jsonl",
"runtimeCards": "runtime/cards.jsonl"
}
}
runtime/cards.jsonl can contain:
{
"id": "basic-0001/front-back",
"noteId": "basic-0001",
"deckPath": ["Basics"],
"kind": "recall",
"front": [
{"kind": "text", "text": "What command builds a Rust project?"}
],
"back": [
{"kind": "code", "language": "shell", "text": "cargo build"}
],
"answer": {
"mode": "self-rating"
},
"fingerprint": "sha256:<64 lowercase hex chars>"
}
This is the shape a simple static app should be able to study.
Final Mental Model
OpenDeck v3 is not "Anki but JSON." It is:
- a JSON/JSONL package format;
- a canonical note/card/asset/source model;
- a resolved runtime-card contract for apps;
- a folder-or-plain-ZIP portability story;
- a typed block renderer model;
- a declared capability system for optional interactivity;
- a clear boundary between deck content and user progress.
The main design bet is that rich authoring and simple studying should not use the same interface. records/ serves authors and tools. runtime/cards.jsonl serves apps.