Skip to content

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 requiredFeatures entry is fatal.
  • Schemas are closed. Unknown properties outside extensions are 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, optional imageId, optional bounded external HTTPS externalImageUrl, servings/yield, source, prep/cook time, rating, difficulty, summary, and timestamps.
  • ingredients: id, recipeId, values, optional parent ingredient, divider state, sort order, and optional canonical ingredientRef, ingredientKey, and normalizationVersion. A personal reference names an archive-local user-catalog ingredient; a standard reference names the stable standard_ingredients document 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 createdAt and updatedAt values are preserved.
  • Missing legacy timestamps are null; export does not invent history.
  • exportedAt and future import-operation timestamps are trusted server times.
  • A future trusted restoredAt may record restoration separately and must not overwrite historical updatedAt.
  • 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.