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 .zip archive 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.jsonl is the resolved app-facing study data.
  • Local media files are ordinary package files, usually under media/, and are indexed by records/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:

LayerFilePurpose
Package metadatadeck.jsonIdentity, schema version, package profile, minimum renderer profile, optional counts, entrypoints
Canonical sourcerecords/*.jsonlNotes, cards, assets, sources
Local binary assetsmedia/ or another package-relative pathImages, audio, video, fonts, datasets, and other files indexed by records/assets.jsonl
Runtime outputruntime/cards.jsonlFully resolved cards for study apps
Optional supportpresentation.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:

  • schema lets validators reject unsupported packages.
  • id is stable across revisions, so apps can associate progress with the same deck over time.
  • revision changes when deck content changes.
  • languages is a list because decks are often bilingual or multilingual.
  • profiles.package tells an app whether it can study the package directly.
  • profiles.minimumRenderer tells the app the lowest renderer profile that can present a reviewable version of the deck.
  • entrypoints should only list files or directories actually included in the package.
  • counts is 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 include runtime/cards.jsonl and 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.
  • kind is 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.
  • fingerprint lets apps notice review-relevant content changes. Real fingerprint values use the form sha256:<64 lowercase hex chars>.
  • deckPath models subdecks without Anki-style delimiter strings.
  • fieldRef keeps canonical cards compact and editable.
  • fieldRef is 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.fieldPresent or when.fieldEmpty;
  • no dependency on records/notes.jsonl;
  • card order matches canonical order where possible;
  • if no explicit non-negative integer order exists, JSONL line order is the default import/new-card order.

Blocks

Blocks are the portable rendering unit. Core block kinds include:

  • text
  • markdown
  • code
  • image
  • audio
  • video
  • math
  • table
  • link
  • group
  • occlusion
  • widget
  • legacyHtml
  • fieldRef for 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 sha256 values use the form sha256:<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, and bytes.

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-rating
  • typed
  • choice
  • draw

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 .zip archive 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.json is 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 legacyHtml with 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.

Built with LogoFlowershow