Donki Deck Format (.donki)

The .donki file format is used for exporting and importing flashcard decks in the Donki application. It is a standard JSON file that contains all necessary metadata, text content, embedded media, and prompts for a deck of cards.

This document serves as a specification for the format, making it easy to programmatically generate or parse .donki files.

The published version of this document is at donki.3sln.com/docs, rendered from this file at build time, and served unrendered at /llms-full.txt. There is no second copy to keep in sync.

Overall Structure#

A .donki file is a JSON object with the following top-level structure:

{
  "type": "deck",
  "urn": "urn:donki:anon:<client-uuid>:<unique-id>",
  "name": "My Vocabulary Deck",
  "createdAt": 1700000000000,
  "updatedAt": 1700000000000,
  "cards": [
    { /* Card Object 1 */ },
    { /* Card Object 2 */ }
  ]
}

Deck Fields#

  • type: (Required) Must be exactly the string "deck".
  • urn: (Required) A universally unique string identifier for the deck. The standard format is urn:donki:anon:<client-uuid>:<unique-id>.
  • name: (Required) The display title of the deck.
  • createdAt: (Optional, Recommended) Unix timestamp (in milliseconds) representing when the deck was created.
  • updatedAt: (Optional, Recommended) Unix timestamp (in milliseconds) representing the last time the deck's structural contents (name, membership) were edited. This is used for Last-Write-Wins (LWW) conflict resolution during import.
  • shard: (Optional) The shard key this file's cards belong to, defaulting to "main". This is what lets one deck arrive as several files -- see Sharding.
  • cards: (Required) An array of Card objects (detailed below) that belong to this deck.

(Note: The app internally tracks user study progress as currentSession and lastStudyDate. These are personal scheduling state, not deck content: Donki omits them when exporting, and ignores them on import in favour of the importer's own schedule. Do not rely on them for distribution.)


Card Object Structure#

Each object inside the cards array represents a single flashcard. Note that a single "Card" can actually generate multiple spaced-repetition reviews if it defines multiple "prompts".

{
  "urn": "urn:donki:anon:<client-uuid>:<unique-id>",
  "face": "What is the capital of **Japan**?\n\n![tokyo_map.png](tokyo_map.png)",
  "assets": {
    "tokyo_map.png": "data:image/png;base64,iVBORw0KGgo..."
  },
  "prompts": [
    {
      "type": "anki",
      "answer": "Tokyo"
    }
  ],
  "createdAt": 1700000000000,
  "updatedAt": 1700000000000
}

Card Fields#

  • urn: (Required) A universally unique string identifier for the card. The standard format is urn:donki:anon:<client-uuid>:<unique-id>.
  • face: (Required) The front of the card. Supports GitHub Flavored Markdown.
  • assets: (Optional) A key-value mapping of media files embedded in the card.
    • Keys: Filename strings (e.g., "audio.mp3", "image.png"). These must match the filenames used in the markdown of the face or prompts.answer (e.g., ![image](image.png)).
    • Values: Standard Data URIs (Base64 encoded strings) containing the raw file data (e.g., "data:image/jpeg;base64,...").
  • prompts: (Required) An array of prompt objects representing the questions asked based on the face. Usually just one prompt per card, but multiple are supported.
  • createdAt: (Optional) Unix timestamp in milliseconds.
  • shard: (Optional) Overrides the file's shard key for this one card. Exports use it to keep a multi-part deck's parts distinct through a single file; a file that is itself one shard has no need for it.
  • updatedAt: (Required) Unix timestamp in milliseconds. Essential for LWW merging.

Sharding: A Deck in Several Parts#

A .donki file does not have to be the whole deck. It can be one shard of it: the same deck urn, plus a shard key naming the part this file carries. Import stores every card under that key and reconciles only the shards the file names, so a second file can extend a deck that already exists -- without exporting it, editing it, and importing the whole thing back.

{
  "type": "deck",
  "urn": "urn:donki:anon:user123:deck456",
  "name": "Japanese Basics",
  "shard": "verbs",
  "cards": [ /* just the verbs */ ]
}

Upload "shard": "verbs", then "shard": "adjectives", and the deck holds both. Neither file needs to know the other exists.

What importing a shard does#

  • The file's cards are written into its shard, by the same last-write-wins rule as before: a card is replaced only when its updatedAt is newer than the copy already stored.
  • Cards that shard held and this file no longer lists are removed, along with their review history. That is what makes re-uploading a corrected part idempotent rather than cumulative: the file is the shard's contents, not an addition to them.
  • Every other shard is left alone. A shard can only ever remove its own cards, so uploading part two cannot disturb part one, and neither can disturb a card written by hand in the app.
  • Deck details still follow updatedAt. A file whose deck-level updatedAt is older than the stored copy will not rename the deck -- but its cards still land, because membership is the sum of the shards rather than a detail of the deck.

The importer states all of this before it commits: which shards the file covers, and how many cards it will add, update and remove.

The default shard#

A file with no shard key covers main, which is also where cards created in the app are put. That is what keeps an ordinary export behaving exactly as it always has -- it replaces main, and a deck that has only ever had main is a deck the whole file describes.

Cards stored before sharding existed carry no key and are read as main, so an existing library needs no migration.

Exports carry the parts back out#

Exporting a deck writes one file holding every card, each with its own shard key and no deck-level key at all. Re-importing that file restores every shard exactly as it was, so an export is still a complete backup of a sharded deck. It also covers every shard, which means importing it reconciles every shard -- a full replacement, which is what a backup should be.


Prompts#

Donki supports multiple types of testing interfaces. A card must define at least one prompt.

1. Standard Anki (Self-Rated)#

The user is shown the face, thinks of the answer, clicks "Show Answer", and then self-rates their memory.

{
  "type": "anki",
  "answer": "The **answer** is here.\nIt supports markdown too!"
}
  • type: Must be "anki".
  • answer: A Markdown string revealed when the user flips the card.

2. Typed Answer#

The user is shown the face, types their answer into a text field, and submits (or presses Enter). Donki compares their input to answer and shows a ✓/✗ hint, then reveals the correct answer (rendered as Markdown) and the self-rating buttons.

{
  "type": "typed",
  "answer": "Konnichiwa"
}
  • type: Must be "typed".
  • answer: The expected answer. Matching is case-, accent-, and whitespace-insensitive (e.g. "Café" accepts "cafe"). The check is only a hint — the learner still self-rates against the exact answer shown, so spelling/accent precision is ultimately up to them.

(Great for vocabulary recall and, combined with a @tts(...) face, for listening/dictation practice.)

3. Multiple Choice#

The user is shown the face, along with a question and multiple choice buttons. Distractors (wrong answers) are automatically sourced from other Multiple Choice cards in the deck or globally.

{
  "type": "multiple-choice",
  "question": "Which city is the capital?",
  "answer": "Tokyo"
}
  • type: Must be "multiple-choice".
  • question: Plain text question displayed above the choices.
  • answer: Plain text string representing the exact correct option.

(Note: Donki automatically populates up to 4 incorrect choices by shuffling answer fields from other multiple-choice cards sharing the same question first from the same deck, then globally, and finally randomly from any multiple-choice cards if needed).


Special Markdown Features#

Because the face and anki answer fields are rendered as Markdown, you can utilize standard formatting (**bold**, *italic*, lists, tables). A card is a small document, so a single newline is a line break rather than a paragraph continuation. Task list items (- [x] done) render their box as a glyph and are not clickable -- a card is something you read, not a form. Donki also extends this with custom components:

Text-to-Speech (TTS)#

You can embed spoken audio dynamically using our custom widget markdown syntax:

@tts(こんにちは){"lang": "ja-JP"}

Options (JSON):

  • lang: A BCP 47 language tag string (e.g., "en-US", "ja-JP"), or an array of tags in order of precedence (e.g., ["ja-JP", "en-US"]).
  • pitch: (Optional) 0.0 to 2.0 (default 1.0). Values outside the range are clamped.
  • rate: (Optional) 0.1 to 10.0 (default 1.0). Values outside the range are clamped.
  • hideText: (Optional) true renders a speaker button on its own instead of the spoken text plus a badge. Use it for listening practice, where showing the text would give the answer away.

A note on lang: which voices exist is the device's business, not the deck's. If none of the tags you name is installed but the device has another voice for the same language -- your card asks for es-MX, the phone has es-ES -- Donki offers that voice as a stand-in and lets the reader accept it once, for the session, or for good. So name the variety you actually mean; being specific costs nothing, and a listener who only has a neighbouring accent is still asked before one is used.

Autoplay Heuristic: If a card face consists entirely of a @tts(...) widget (or standalone <audio>/<video>) with no other meaningful text, Donki will attempt to automatically play the media when the card is shown.

Full File Example#

{
  "type": "deck",
  "urn": "urn:donki:anon:user123:deck456",
  "name": "Japanese Basics",
  "updatedAt": 1718000000000,
  "cards": [
    {
      "urn": "urn:donki:anon:user123:card789",
      "face": "How do you say 'Hello' in Japanese?\n@tts(こんにちは){\"lang\": \"ja-JP\"}",
      "prompts": [
        {
          "type": "multiple-choice",
          "question": "Select the correct reading:",
          "answer": "Konnichiwa"
        }
      ],
      "assets": {},
      "updatedAt": 1718000000000
    }
  ]
}

Generating Decks with an LLM Agent#

Authoring decks this way is what the format is shaped for. A whole deck is one JSON document with a small required surface, the card content is Markdown, and every identifier is an opaque string the model can invent — so "make me a deck for the JLPT N5 verb list" is a single response rather than an integration.

Three properties do most of the work:

  • Nothing has to be fetched or encoded. assets is optional, and @tts(...) is rendered by the device at review time rather than shipped as audio. A model with no file access and no tools can still produce a complete deck, spoken cards included.
  • One face, several reviews. A card's prompts array lets the agent get recognition, spelling recall and listening practice out of a single item it only had to write once. Multiple choice needs no distractors either — Donki assembles those from the rest of the deck.
  • A deck can be produced in parts. Give each response its own shard key and a long deck becomes several files, uploaded independently and regenerated one at a time -- see Sharding. A part that comes out wrong is re-uploaded on its own, without regenerating or even reading the rest.
  • Regenerating is an edit, not a duplicate. Import merges by urn and keeps whichever copy carries the newer updatedAt. If a second pass reuses the URNs from the first and bumps the timestamps, it corrects the deck in place — and because study progress lives outside the file, your review history survives the correction.

The smallest valid deck#

Everything else in this document is optional. This is the whole requirement:

{
  "type": "deck",
  "urn": "deck-0e0b3f",
  "name": "One Card",
  "cards": [
    {
      "urn": "card-8a41c2",
      "face": "What is the capital of Japan?",
      "prompts": [{ "type": "anki", "answer": "Tokyo" }],
      "updatedAt": 1718000000000
    }
  ]
}

Rules an agent has to follow#

  • type must be exactly "deck" and cards must be an array. Anything else is refused at import, before a single card is written.
  • Every deck and every card needs a unique urn. Any string will do — the urn:donki:anon:<client-uuid>:<unique-id> convention is a convention, not a requirement, and one UUID per card is enough. A card with no urn is skipped; the rest of the deck still imports, which makes a missing identifier a quiet failure rather than a loud one.
  • Give every card an updatedAt in milliseconds. Merging is decided on it, and a card without one can never replace a card that has one.
  • prompts must hold at least one entry.
  • Do not invent wrong answers for multiple choice. Cards sharing an identical question string pool their distractors, so keep that string byte-identical across the cards that belong to one question, and vary only the answer.
  • Emit strict JSON: no trailing commas, no comments, no unquoted keys.
  • Leave assets out unless an image or a recording genuinely belongs on the card. A base64 data URI in the middle of generated output is where a long response goes wrong.

A prompt to start from#

Write a Donki deck as a single .donki file (JSON) about <topic>.

Follow the specification at https://donki.3sln.com/docs/ exactly:
  - Top level: {"type": "deck", "urn", "name", "updatedAt", "cards": [...]}
  - Every deck and every card needs a unique "urn" and a
    millisecond "updatedAt".
  - Each card: {"urn", "face" (Markdown), "prompts": [...], "updatedAt"}
  - Prompt types: "anki" (self-rated), "typed" (spelling recall), and
    "multiple-choice" (needs "question" and "answer" -- never write the
    wrong answers, Donki supplies those).
  - For pronunciation, put @tts(<text>){"lang": "<BCP 47 tag>"} in the
    face. Add "hideText": true where showing the text would give the
    answer away.

Produce 20 cards. Output the JSON and nothing else.

Getting the file into Donki#

  1. Copy the JSON and paste it: open Import and use Paste from Clipboard, or press Ctrl+V (⌘V on a Mac) if you have a keyboard, or paste into the box the importer offers when the browser won't hand the clipboard over. Nothing has to be saved anywhere first, which makes this the shortest path from a chat window to a deck — and a copied .donki file pastes just as well as the text.
  2. Or save the output with a .donki extension and choose Upload File.
  3. Or put it anywhere it can be fetched — a gist, an object store, a static host — and paste that address into Load URI. The importer also accepts data: and blob: addresses, which is the shortest path for an agent that can produce a link but not a file.
  4. Or link straight at the importer: https://donki.3sln.com/app/import?uri=<url-encoded address> opens Donki with that deck already loaded.

Whichever route, Donki shows the deck's name and card count and waits for confirmation before writing anything, so a malformed run is caught before it reaches your library.