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
usersaccount/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:
{uid}equals the authenticated exporting user.{recipeId}resolves to a recipe in a box owned by that user.- The recipe's
image_urlreferences 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.