All formats

"Export everything": all data in open formats, version 1

Status: draft until app version 1.0 ships. Issue #90, decision 0044. Reference implementation: Packages/KuriosaKit/Sources/KuriosaStore/FullExport/ (FullExport builds the contents, EncryptedZipWriter writes the ZIP file), the app's AppModel+FullExport.swift and ExportEverythingSheet.swift.

A person's data must stay readable without Kuriosa. Settings › Backup › "Export everything …" writes one ZIP file with every piece, wish, and collection in open formats (a spreadsheet, CSV, and JSON) and every photo and document as Kuriosa keeps it. The ZIP is encrypted with a password the person chooses, so it opens with free tools (7-Zip, Keka, The Unarchiver) without Kuriosa.

The contents are a function of the catalog (a SQLite database, catalog-schema.md), the media files, the language, the time of the export, and whether sensitive fields are included. Everything below is defined on the catalog's rows, so another program that reads a backup (backup-format.md) can write exactly the same files. The free tool "Open a backup without Kuriosa" on kuriosa.app does (section 9).

1. The ZIP file

Anyone with the file can read the names inside it without the password, so they are neutral: numbers, never the names of pieces, collections, or documents (owner's choice on issue #90).

2. Terms used below

3. What is exported, and its order

Records with deleted_at set are never exported (their changes are in the history).

The media file of a photo is media/<id>.enc, of a cut-out media/<id>.cutout.enc (KEF1 contexts media:<id> and cutout:<id>, library-format.md). A file that cannot be read is left out of the ZIP and named to the person; the tables and the JSON still list it.

4. Sensitive fields

Fields marked sensitive in a template are left out unless the person switched on "Include sensitive fields" for this export (after the warning and Face ID or the passcode, as for every export, export.md section 3). Left out means:

kuriosa.json says which: "includesSensitiveFields": false.

5. Kuriosa.xlsx

One workbook (Office Open XML, written as described in export.md section 4a) with these worksheets in this order; row 1 holds the column labels (bold, frozen):

  1. Collections (text "Collections"): one row per exported collection.
  2. One sheet per collection, named after the collection.
  3. Wishlist, Journal, Links, Sets, Places, Tags (their texts).

Sheet names are made fit for Excel (at most 31 code points, none of [ ] : * ? / \, no apostrophe at either end, not "History"; else "Kuriosa"). The fixed sheets' names are taken first; a collection's name that is already taken (ignoring case) gets (2), (3), … (the name cut to leave room).

Cells: text is a shared string (an empty text is no cell); numbers are written as their number text or JSON literal; days are dates (serial numbers from 1 March 1900 on, else text YYYY-MM-DD); amounts are numbers with their currency in the number format ("CHF "#,##0.00). Column widths follow the longest value in code points (10 to 60).

Columns

Collections: Collection (path) · Kind (the template's name, resolved) · Unit (the template's quantity.other, resolved; empty when the collection does not count) · Pieces (the number of its exported pieces, not counting sub-collections) · Sheet (its sheet's name) · CSV file (CSV/Collection <n>.csv) · Cover photo (Photos/Collections/<n>.jpg, or the file of the piece photo named by cover_media_id when that photo is exported, else empty) · ID.

A collection's sheet (its pieces): Inventory number · Title · Status · Place (path) · Purchase date · Purchase price · Source · Estimated value · Condition · Tags (names in code point order, joined by , ) · Favorite (Yes/No) · Notes · the fields (below) · Photos · Files · Codes · Added (created_at as stored) · ID.

Wishlist (the wishes): Title · Collection (path) · Priority (High, Medium, Low for 1, 2, 3) · Target price · the wishlist template's fields · the collection fields of the wishes (below) · Estimated value · Tags · Favorite · Notes · Photos · Files · Added · ID.

Journal (pieces in export order; per piece its first purchase, when purchase_date, purchase_price_*, source, or purchase_quantity is set, then its events in stored order): Inventory number · Title · Date · Entry (Purchase, Used, Sold, Given away, Lost, Lent out, At service, Returned) · Count · Amount · Other party (counterparty; the first purchase's source) · Due back · Note.

Links: Piece (item label of from_item_id) · Link ("Part of" for part_of, "Linked with" for mounted_on) · Other piece (item label of to_item_id).

Sets: Set (name) · Pieces (item labels, one per line, in export order) · Notes.

Places: Place (path) · ID. Tags: Tag · ID.

Photos: the photo files, one per line (Photos/K-0001/1.jpg), each photo's cut-out on the line after it. Files: one per line, the file's path and, when the document has a name, its name in brackets (Files/K-0001/1.pdf (Invoice 2023.pdf)). Codes: item_code.payload, one per line.

Fields

The fields of a template, as columns: every field that is not a calculation (calculated values are not stored; the template in kuriosa.json holds the formula) and not left out as sensitive, first those without hidden in template order, then the hidden ones (removed in "Edit fields") in template order. The label is the field's label, resolved; a hidden field's label is "<label> (removed)"; a measurement adds (mm), (g), or (ml) (values stay in these units), a number with a suffix adds (<suffix>). A quantity field has two columns: the amount, and "<label> (unit)" with the unit option's label (or the stored unit when it is not an option).

On the wishlist, the wishlist template's fields read the wishes' wish_fields; then come the fields of the wishes' collection templates, read from fields: the templates of the wishes in export order, each template's fields as above, a key once (the first template that has it gives its label and type). A wish's cell is empty when its own template does not have the field, or left it out as sensitive.

Values, by the type of the stored value ({"type", "value"}, catalog-schema.md):

TypeCell
text, urlthe text
number, measurement, ratingthe number literal
datethe day
currencythe amount
dropdown, multiple_choicethe option labels (resolved, in the order of the field's options; an unknown ID as itself), joined by , ; either kind of value in either kind of field
checkboxYes or No
quantitythe amount literal, and the unit's label in the unit column
relationthe item label of the item it names, if exported, else the stored ID

A value of another type than the field's (other than the two choice types) leaves the cell empty; kuriosa.json keeps it.

6. CSV files

CSV/Collections.csv, CSV/Collection <n>.csv for each exported collection, then CSV/Wishlist.csv, CSV/Journal.csv, CSV/Links.csv, CSV/Sets.csv, CSV/Places.csv, and CSV/Tags.csv: the same rows and columns as the worksheets, in every language with a comma between fields and a dot for decimals, so every program reads them alike. UTF-8 with a byte order mark, CRLF line endings, fields quoted as in RFC 4180 (export.md section 4).

7. kuriosa.json

UTF-8 without byte order mark, written as follows (Kuriosa and other implementations must write the same bytes): two spaces of indentation per level, a line break after {, [, and each member, ": " between a key and its value, {} and [] for empty objects and arrays, a final line break. Strings escape " and \, use \b, \f, \n, \r, \t, and \u00xx (lowercase) for other characters below U+0020, and keep every other character. Keys appear in the order listed here; objects copied from the catalog keep their stored order.

{
  "format": "kuriosa-export",
  "formatVersion": 1,
  "exportedAt": "2026-10-11T08:30:00.000Z",
  "language": "en",
  "libraryID": "<uuid>",
  "catalogVersion": 16,
  "includesSensitiveFields": false,
  "collections": [ … ],
  "templates": [ … ],
  "places": [ … ],
  "tags": [ … ],
  "items": [ … ],
  "links": [ … ],
  "sets": [ … ],
  "exportTemplates": [ … ],
  "libraryMeta": { … },
  "history": [ … ],
  "backups": [ … ]
}

exportedAt is the time of the export (UTC, milliseconds); catalogVersion the catalog's PRAGMA user_version. Every member below is present; what the catalog does not have is null. Timestamps and days are the stored text. A money value is {"minorUnits": <integer>, "currency": "<code>"}. Copied JSON (templates, fields, events, …) is the catalog's JSON, parsed and written in the form above; a column that does not parse counts as empty ({}, []).

Never exported: item_search (derived), link_preview (pages loaded from the internet), and thumbnails (derived).

8. Read me.txt

UTF-8 without byte order mark, CRLF line endings. A block in the export's language, then, when the language is not English, CRLF, a line ----------, an empty line, and the same block in English. A block is these paragraphs (texts of FullExportText), separated by an empty line and ended by a line break:

  1. "Kuriosa: everything in your collection", a line break, and "Exported on %@." with the day of the export (YYYY-MM-DD, the device's time zone);
  2. the introduction, the spreadsheet, the CSV files, the JSON file, the photos, the files, and the units (readMeIntro … readMeUnits);
  3. "Sensitive fields … are left out." or "… are included. Keep this file safe.";
  4. "More about your data without Kuriosa: %@" with https://kuriosa.app/help/your-data/, or https://kuriosa.app/<language>/help/your-data/ for another language.

%@ is replaced by its value (the first %@ or %1$@ of the text).

9. The tool "Open a backup without Kuriosa" (part 2 of issue #90)

A page on kuriosa.app (/tool/ and the same in every language), also downloadable as one HTML file (/downloads/kuriosa-open-backup.html, with its SHA-256 on the page and under /formats/), opens a backup (backup-format.md) with its recovery key and writes the files above. Reference implementation: website/src/tool/kuriosa-open.js (the reader, published as /tool/kuriosa-open.js with the fixed texts of every language from the app's String Catalog) and website/src/tool/kuriosa-tool.js (the page). It uses no other code; the browser's Web Crypto does the cryptography (HKDF-SHA256, AES-256-GCM, SHA-256).

Kuriosa's test BackupToolTests runs the published reader in JavaScriptCore with CryptoKit in place of Web Crypto, opens the golden backup Fixtures/golden/export-v1/Sample.kuriosabackup in every language, and requires every file to equal the app's (the workbooks part by part). It also requires the reader's catalogVersion to be the app's current catalog version: a new catalog version needs the reader and the golden backup to follow.