MyRecipes structured user-backup archive v1
Archive version 1.1 extends the v1 format with canonical ingredient references, personal ingredient identity metadata, and user ingredient costs. Version 1.0 archives remain importable; omitted references and costs are treated as legacy data rather than synthesized.
Issue: #375. The machine-enforced contract is implemented in
functions/src/user_backup_archive.ts. This document describes the portable
format used by future export (#378) and replacement import (#379/#381).
This is not the readable cookbook PDF format and is not a third-party recipe import format.
Archive layout
my-recipes-YYYY-MM-DD.zip
├── manifest.json
├── README.txt
├── data/
│ ├── recipe-content.json
│ ├── user-catalog.json
│ └── images.json
└── images/
├── image-1.jpg
└── image-2.png
All names are UTF-8. Only regular files in this fixed layout are accepted. The README must state that import is full replacement, not merge.
Version and compatibility
{
"format": "myrecipes-export",
"exportVersion": {"major": 1, "minor": 1},
"requiredFeatures": [],
"extensions": {}
}
- An unknown major version is rejected.
- A newer v1 minor version is accepted only if every required feature is understood.
- V1 currently defines no required feature extensions; any nonempty unknown
requiredFeaturesentry is fatal. - Schemas are closed. Unknown properties outside
extensionsare rejected. - Unknown optional extension namespaces may be ignored with a warning and are not guaranteed to survive import/re-export.
- Extensions cannot control ownership, memberships, authorization, limits, Firestore paths, buckets, or Storage destinations.
Manifest
manifest.json contains:
| Field | Meaning |
|---|---|
format |
Required literal myrecipes-export |
exportVersion |
Major/minor format version |
status |
complete or incomplete |
exportedAt |
Trusted server RFC 3339 UTC time |
applicationVersion |
Backend exporter revision. Deployed workers use Cloud Run's platform-provided K_REVISION; local/emulator exports use local-development. This is not the requesting Flutter client version. |
recipeCount, imageCount |
Counts verified against structured files |
includedDataTypes |
Exactly recipe-content, user-catalog, and images in v1 |
requiredFeatures |
Features an importer must understand |
extensions |
Namespaced optional extensions |
issues |
Bounded machine-readable omissions for an incomplete export |
files |
Path, expanded byte length, and lowercase SHA-256 for every other entry |
The manifest does not checksum itself. V1 does not use an application signature or MAC. Checksums detect corruption, not archive authorship. Import treats every field as untrusted and assigns ownership and destination paths in trusted backend code.
A complete archive has no issues. An incomplete archive has at least one issue, may be downloaded only with a prominent warning, and cannot be imported or used as a pre-replacement safety export.
Structured data
Every structured file contains:
{"schemaVersion": {"major": 1, "minor": 1}}
All IDs are globally unique archive-local strings. They match
^[a-z][a-z0-9-]{0,79}$. Original Firestore document paths and numeric IDs are
not required and cannot establish ownership.
data/recipe-content.json
This file contains these closed arrays:
recipeBoxes:id,name,createdAt,updatedAt.recipes:id,recipeBoxId,name, optionalimageId, optional bounded external HTTPSexternalImageUrl, servings/yield, source, prep/cook time, rating, difficulty, summary, and timestamps.ingredients:id,recipeId, values, optional parent ingredient, divider state, sort order, and optional canonicalingredientRef,ingredientKey, andnormalizationVersion. A personal reference names an archive-local user-catalog ingredient; a standard reference names the stablestandard_ingredientsdocument ID.directions:id,recipeId, group, step number, and instructions.notes:id,recipeId, title, text, type, privacy state, and timestamps. Ordinary and private Firestore note collections share this portable schema.categories:id,recipeId, and name.userMetadata:id,recipeId, and string values.
Every recipe must reference an included box. Every child and personal-metadata
record must reference an included recipe. Ingredient parent IDs must reference
an ingredient in the same recipe. Every non-null recipe imageId must resolve
through data/images.json.
Ownership fields, share lists, collaborator data, email addresses, Firestore
paths, raw image_url fields, buckets, and Storage destinations do not exist in
the portable schema. An imported recipe's external HTTPS image reference may be
carried in externalImageUrl; Firebase Storage URLs, local paths, credentials,
and non-HTTPS references are prohibited. Older v1 archives without this optional
field remain valid.
data/user-catalog.json
measures: archive-local ID, full name, abbreviation, optional type, and timestamps.ingredients: archive-local ID, name, timestamps, and optional ingredient key, normalization version, and status. The optional fields are absent in legacy 1.0 archives.costs(added in 1.1): archive-local ID, canonical ingredient reference, copied name and key, optional brand/store, package price, quantity and unit, preferred status, normalization version, and timestamps. Every ingredient represented in this array must have exactly one preferred cost.
On replacement, personal references are remapped to the newly created
recipe_user_ingredients document ID. Standard references remain pointed at
the existing system catalog. A missing personal target, malformed reference,
or invalid preferred-price set rejects the archive.
Standard measures, ingredients, and categories are referenced values, not user-owned catalog documents, and are not represented here.
data/images.json
Each image contains:
{
"id": "image-1",
"recipeId": "recipe-1",
"path": "images/image-1.jpg",
"contentType": "image/jpeg",
"byteLength": 12345,
"sha256": "64 lowercase hexadecimal characters"
}
Allowed media are JPEG (.jpg) and PNG (.png). Path, MIME type, extension,
byte length, SHA-256, and JPEG/PNG byte signature must agree. The bytes—not a
temporary URL—are included as a regular archive entry.
Timestamp semantics
- Timestamps are RFC 3339 UTC using
Z, with up to nanosecond text precision. - Valid original
createdAtandupdatedAtvalues are preserved. - Missing legacy timestamps are
null; export does not invent history. exportedAtand future import-operation timestamps are trusted server times.- A future trusted
restoredAtmay record restoration separately and must not overwrite historicalupdatedAt. - Archive timestamps never control authorization, entitlement, retention, or operation expiry.
Limits
| Limit | V1 value |
|---|---|
| Compressed ZIP size | 2.5 GiB |
| Total expanded size | 2.5 GiB |
| Total image bytes | 2 GiB |
| Individual image | 5 MiB |
| Structured JSON total | 256 MiB |
| Individual structured JSON file | 128 MiB |
| Manifest or README | 1 MiB each |
| ZIP entries | 1,100 |
| Recipes | 1,000 |
| Images | 1,000 |
| Path length | 240 UTF-8 bytes |
| Directory depth | 2 below archive root |
| Per-entry and aggregate compression ratio | 200:1 |
| Nested archives | Prohibited |
Existing ACCOUNT_LIMITS also apply: recipe boxes, total owned recipes, and
ingredients, directions, categories, and notes per recipe. Export must not
generate an archive rejected by the corresponding released importer.
Validation and hashing must stream data. These limits do not authorize loading an entire archive into memory or transporting it in a callable request body.
Path and entry safety
Validation occurs before extraction. Reject:
- absolute, drive-letter, UNC, backslash, empty-segment, dot, and traversal paths;
- invalid percent encoding, disguised traversal, NUL, control characters, and invalid UTF-8;
- duplicate raw paths, Unicode NFC collisions, case-folded collisions, and file/directory conflicts;
- symlinks, hard links, devices, FIFOs, sockets, and other non-regular entries;
- nested archives and files outside the fixed layout.
Original filenames never select destination Storage paths.
Empty libraries
A complete archive may contain zero recipes and zero images. Replacement still requires validation, a verified safety export, and explicit confirmation, and removes/quarantines in-scope content absent from the archive. Excluded data and memberships on boxes owned by other users remain unchanged. Imported boxes are private under the approved #381 v1 sharing policy.
The archive should normally carry the user's empty default recipe box. The application contract requires at least one recipe box, so a complete archive with zero boxes is normalized during replacement to one empty private default box. Validation preview and confirmation disclose this, expected post-import counts include the normalized box, and the completion report records it. The active dataset equals the normalized interpretation of the archive.
Validator API
ZIP parsing supplies regular-file entries to:
validateBackupArchive(entries, {forImport: true});
The validator performs no Firebase reads or writes. It checks paths before parsing, validates all closed schemas, verifies manifest and image checksums, resolves relationships, enforces counts/limits, and rejects incomplete imports. Export/import orchestration must still authenticate users and apply the #374 ownership boundary.