Skip to content

Recipe backup and replacement scope

Issue: #374. This document is the human-readable companion to functions/src/recipe_backup_scope.ts, which is the authoritative typed v1 scope consumed by future export and replacement work.

Boundary

V1 backs up and replaces recipe content owned by the authenticated user. A collaborator's ability to read a shared recipe box is not ownership. The archive never controls ownership, memberships, Firestore paths, or Storage paths.

Firestore collection Included ownership proof Query/dependency Replacement
recipeboxes owner_uid == authenticated uid Query owner_uid Replace owned boxes; imported boxes are private under #381
recipes recipe_box_id resolves to an included owned box Query for owned box IDs Replace recipes in owned boxes; creator_uid is not ownership
recipe_ingredients recipe_id resolves to an included recipe After recipes Replace
recipe_directions recipe_id resolves to an included recipe After recipes Replace
recipe_notes recipe_id resolves to an included recipe After recipes Replace ordinary/legacy public notes
private_recipe_notes recipe_id resolves to an included recipe After recipes Replace owner-only notes
recipe_categories recipe_id resolves to an included recipe After recipes Replace
recipe_user_metadata user_uid == authenticated uid and recipe_id resolves to an included owned recipe After recipes Replace only metadata for owned recipes
recipe_user_measures user_id == authenticated uid Query user_id Replace
recipe_user_ingredients user_id == authenticated uid Query user_id Replace
recipe_user_ingredient_costs user_id == authenticated uid Query user_id; references a personal or standard ingredient Replace and preserve exactly one preferred cost per ingredient

Every exported relationship receives an archive-local ID in #375. Original document IDs and numeric business IDs may be retained for traceability but are not ownership evidence. Import assigns the authenticated user in trusted backend code.

Within Firestore, the authoritative logical keys are recipeboxes.id to recipes.recipe_box_id and recipes.id to each child recipe_id. Numeric and string representations of the same valid stable ID may be normalized for comparison. A Firestore document-ID match must be reported separately and must not silently make a broken business-ID relationship valid. Missing and duplicate business IDs are validation failures.

Collection fields are not yet the portable archive schema. In particular, current records use several conceptual ownership names and do not consistently carry createdAt, updatedAt, or schemaVersion. Issue #375 must define the normalized archive fields and legacy/default timestamp behavior. Import must write trusted ownership and schema metadata according to the then-current server model. This inconsistency does not prevent server-verifiable ownership, so #374 does not require a Firestore backfill.

Personal metadata belonging to a collaborator is never included in the owner's export. Conversely, an importing user's metadata that references a recipe owned by somebody else is out of scope and remains unchanged.

Referenced, not owned

recipe_measures, standard_ingredients, and valid_categories are system catalogs. An archive may preserve the display values needed by a recipe, but must not represent or restore these catalog documents as user-owned records. Canonical references to standard_ingredients retain the system document ID. References to recipe_user_ingredients use archive-local IDs and are remapped to the newly restored personal ingredient document IDs during replacement.

Explicit exclusions

Import must neither accept nor alter these categories:

  • Firebase Authentication and users account/profile data.
  • Shopping lists, aisle maps, shopping items, and shopping catalogs/templates.
  • Shopping-list invitations. Recipe-box sharing currently uses membership fields rather than a separate invitation collection; archive membership claims are never restored.
  • Purchase transactions/events, subscriptions, and entitlement sources/grants.
  • Usage, quota, rate-limit, and image-upload reservation records.
  • Administrative, entitlement, identity-security, App Check, and other audit or operational records.
  • Current memberships on recipe boxes owned by the importing user. Under the approved #381 v1 replacement policy, imported boxes are private and existing collaborator access to replaced boxes is removed after explicit disclosure. Memberships on boxes owned by other users remain unchanged.

The account-deletion traversal is broader by design and must not be reused as the export/import allowlist.

Recipe images

The only current owned path is:

users/{uid}/recipes/{recipeId}/{objectId}

Ownership requires all three checks:

  1. {uid} equals the authenticated exporting user.
  2. {recipeId} resolves to a recipe in a box owned by that user.
  3. The recipe's image_url references that exact object.

Custom object metadata is supplemental and never overrides this chain. No metadata backfill is required for v1. Export calculates its own content hash from downloaded bytes.

Image case Classification and behavior
Canonical object passing all ownership checks Include bytes; replace with the owning recipe
images/place-holder.png Shared reproducible asset; exclude from user archive and replacement
Empty or missing reference/object Mark missing; #375 decides fatal archive semantics
External HTTPS URL Preserve the bounded URL as portable recipe content; do not claim or package its bytes
External HTTP URL, bundled asset, or local path Exclude; it is neither a safe portable reference nor an owned object
Legacy Storage URL/path Do not assume ownership; migrate or explicitly report/exclude after relationship validation
Cross-tenant or recipe-mismatched path Ownership violation; reject and never export/delete it
Orphaned canonical object not referenced by its recipe Outside export; flag for separate cleanup rather than silently claiming it

Deletion is performed by the trusted delete_recipe backend operation. It marks the recipe as deleting, removes every recipe child plus metadata for all users, and deletes the recipe parent last; retrying is idempotent. Recipe-box deletion uses the same child-first order. Owned image cleanup is recoverable/asynchronous and must use the same verified relationship. User-created ingredient/measure catalogs are deleted independently by their direct owner field. Creation/import uses the reverse dependency order.

Sharing and test fixtures

The scope test fixture contains two owners and reciprocal shared-box access. It also intentionally puts a recipe whose creator_uid differs from the box owner into each box. The assertions prove that:

  • box ownership, not membership or creator_uid, determines recipe ownership;
  • a member cannot export another owner's box, recipe children, or images;
  • only the owner's metadata on the owner's recipe is included; and
  • metadata referring to somebody else's recipe remains outside replacement.

Index and migration assessment

The required inventory queries use existing single-field equality indexes on owner_uid, recipe_box_id, recipe_id, user_uid, and user_id. Firestore creates these single-field indexes automatically; #374 requires no new composite index. Existing canonical image paths and relational ownership are sufficient, so no ownership-field or Storage-metadata migration is required.

If implementation later adds ordered pagination combined with these filters, that issue must add and deploy the corresponding composite index before relying on the query in production.