"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
- Name:
Kuriosa <YYYY-MM-DD>.zip(the day of the export on the device). - Entries, in this order:
Read me.txt,kuriosa.json,Kuriosa.xlsx, the CSV files (section 6), the collections' own cover pictures, then each exported item's photos (with their cut-outs) and files, items in export order (section 3). - No folder entries; names are UTF-8 (general purpose flag bit 11) with
/between folders. - Every entry is encrypted with WinZip AES-256 in the AE-2 form: compression method 99 with the extra field
0x9901(vendor version 2, vendorAE, strength 3, the real method), a random 16-byte salt per entry, the 2-byte password check, AES-256 in CTR mode with a 128-bit little-endian counter starting at 1, and the first 10 bytes of HMAC-SHA1 over the encrypted data. Keys come from PBKDF2-HMAC-SHA1 (password, salt, 1,000 iterations, 66 bytes: the AES key, the HMAC key, the password check). The CRC-32 field is 0, as AE-2 prescribes. - The password is the person's, at least 12 characters, as UTF-8 of its NFC form. Kuriosa never stores it.
Read me.txt,kuriosa.json, and the CSV files are deflated (real method 8) before they are encrypted; the spreadsheet, photos, and files are stored (method 0), as they are compressed already.- ZIP64 records are written only when needed: an entry or an offset of 4 GiB or more, or more than 65,535 entries.
- Time stamps: the time of the export, local time, MS-DOS format. Version made by: Unix (3), 6.3; file attributes
0644.
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
- Code point order: strings compared by their Unicode code points, as SQLite's
BINARYcollation compares UTF-8. - Resolving a localized text (a JSON object of language code → text, see template-format.md) for language
L: the value forL, else foren, else the smallest value in code point order, else"". - Language: the app's language (
en,de,fr,it, ores). Fixed texts ("Title", "Sold", the sheet names, the read-me) are the app's String Catalog entries in that language (FullExportTextlists their English keys). - Path of a collection or place: the names from the top down, joined by
›. - Inventory number text:
K-and the number with at least four digits (K-0007). - Item label: the title, then
(K-0007)when the item has a number (Submariner (K-0007)). - Number text of a real value (a SQLite
REAL): an integral value below 10^15 as an integer (500,-3), else the shortest digits that read back as the same double, in plain decimal notation without an exponent (0.1,0.00001,0.30000000000000004). - Number literals inside JSON columns (
item.fields,item.events, …) are copied as they are written in the catalog (for example40.5or1e-05), never reformatted. - Amount text of minor units in a currency with
dfraction digits (ISO 4217, from the platform's currency data): the sign, the whole units, andddecimals (6450.50,-0.05,1200for JPY).
3. What is exported, and its order
Records with deleted_at set are never exported (their changes are in the history).
- Collections: every collection not deleted, depth first: the top level, then each collection's children, siblings by
sort_index, thenname, thenid(code point order). A collection whose parent is missing or deleted counts as top level. Collectionn(from 1, in this order) has the sheet and file names of section 5. - Pieces: items with
kind = 'owned'in an exported collection, collection by collection in the order above; in a collection bynumber(items without one last), thencreated_at, thenid. Sold, given-away, and lost pieces are included. - Wishes: items with
kind = 'wish'in an exported collection, bycreated_at, thenid. - Export order of items: the pieces, then the wishes.
- Folder of an item: its inventory number text (
K-0001); items without a number getW-0001,W-0002, … in export order. - Photos and files of an item: its
mediarows withkind = 'photo'andkind = 'document'respectively, each bysort_index, thencreated_at, thenid. Photon(from 1) isPhotos/<folder>/<n>.<ext>, its cut-out (whencutout_content_typeis set)Photos/<folder>/<n>-cutout.<ext>, filenisFiles/<folder>/<n>.<ext>. - Extensions by content type:
image/jpeg→jpg,image/png→png,image/heic→heic,application/pdf→pdf, anything else →bin. - Own cover pictures (
collection.cover_picture_id):Photos/Collections/<n>.jpgfor collectionn, the filemedia/<id>.enc. - Places: not deleted, depth first like collections (
sort_index,name,id). - Tags: by
name, thenid. - Links (
relation): those whose two items are exported, bycreated_at, thenid. - Sets (
item_set): not deleted, byname, thenid; their members that are exported. - Codes (
item_code): of exported items, bycreated_at, thenid.
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:
- an item's
fieldslose the keys that are sensitive in its collection's template, itswishFieldsthe keys sensitive in the wishlist template; tables have no column for them; - in the history, every key that is sensitive in any template of the library (
S) is removed: changes whosepathisfields.<key>with a key inSare dropped, and the values of changes whose path isfieldsorwishFieldslose the keys inS.
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):
- Collections (text "Collections"): one row per exported collection.
- One sheet per collection, named after the collection.
- 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):
| Type | Cell |
|---|---|
text, url | the text |
number, measurement, rating | the number literal |
date | the day |
currency | the amount |
dropdown, multiple_choice | the 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 |
checkbox | Yes or No |
quantity | the amount literal, and the unit's label in the unit column |
relation | the 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).
- Days:
YYYY-MM-DD. Numbers: as in the workbook. - An amount is two columns: the amount text and "<label> (currency)" with the ISO code.
- Text with several lines keeps its line breaks inside the quoted field.
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 ({}, []).
collections[]:id,name,path,parentID,templateID,sortIndex,coverPhotoID(cover_media_id),coverPicture(its file in the ZIP, ornull),sheet,csv,createdAt,updatedAt.templates[]: everytemplaterow'sjson, byid.places[]:id,name,path,parentID,sortIndex,createdAt,updatedAt.tags[]:id,name.items[], in export order:id,kind,number(integer),folder,collectionID,title,status,placeID,purchaseDate,purchasePrice(money),source,purchaseQuantity(number text),estimatedValue(money),condition,notes,isFavorite,tagIDs(fromitem_tag, in code point order),wish({"priority", "targetPrice"}for a wish, elsenull),fields,wishFields,events,reminders,primaryPhotoID,backPhotoID,photos,files,codes,createdAt,updatedAt.photos[]:id,file,cutoutFile,showsCutout,contentType,cutoutContentType,width,height,cutoutWidth,cutoutHeight,rotation,crop({"x", "y", "width", "height"}as number texts, ornull),caption,originalName(filename),byteCount,createdAt. Rotation and crop are how Kuriosa shows the photo (catalog-schema.md, "Media edits"); the file itself is unchanged.files[]:id,file,contentType,caption,originalName,byteCount,createdAt.codes[]:id,code(payload),createdAt.
links[]:id,type(partOf: the first item is part of the second;linkedWith),fromItemID,toItemID,createdAt.sets[]:id,name,notes,itemIDs(exported members in code point order),createdAt,updatedAt.exportTemplates[]: everyexport_templaterow'sjson, bycreated_at, thenid.libraryMeta: everylibrary_metarow, key → value, keys in code point order.history[]: everyhistoryrow byoccurred_at, thenid:id,batchID,occurredAt,actor,device,entity,entityID,action,changes(copied, see section 4),undoesID.backups[]: everybackup_eventrow byoccurred_at, thenid:id,occurredAt,kind,fileName,folder,itemCount,photoCount,byteCount,succeeded(boolean),reason.
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:
- "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); - the introduction, the spreadsheet, the CSV files, the JSON file, the photos, the files, and the units (
readMeIntro…readMeUnits); - "Sensitive fields … are left out." or "… are included. Keep this file safe.";
- "More about your data without Kuriosa: %@" with
https://kuriosa.app/help/your-data/, orhttps://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).
- It reads the header, unlocks the data key with the recovery key slot, decrypts the catalog, reads the catalog's SQLite file itself (table b-trees, overflow pages, the records; columns by the tables'
CREATE TABLEtext, a column added later reads itsDEFAULTfor older rows), and builds the export exactly as above in the page's language. - It then reads every entry once, in file order, checks it against the sealed index, and decrypts the photos, cut-outs, and documents the export names (contexts
media:<id>andcutout:<id>). A backup whose index does not match is still opened, with a warning. - Output: in Chrome and Edge into a folder the person chooses (a new folder "Kuriosa <YYYY-MM-DD>" inside it, File System Access); elsewhere one ZIP file to save, stored without compression and without a password (it stays on the computer). The workbook's parts are stored too, so only the ZIP containers differ from the app's.
- Sensitive fields are included unless the person unticks them: the recovery key, which only the owner has, takes the place of Face ID.
- The page loads nothing and sends nothing;
scripts/build_website.py --checkfails if the reader, the page script, or the download contain a network call or another address.
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.