Recipe-image disaster recovery
Issue #376 protects canonical Firebase Storage recipe images against deletion,
overwrite, and loss of the active bucket. Development and recovery testing run
only in myrecipes-test; production is never an exercise destination.
Environment targeting, IAM, deployment, rollback, and monitoring are governed
by the development operations runbook
and production operations runbook.
Selected mechanism
Protection uses three layers:
- Source-bucket object versioning protects overwritten objects.
- Seven-day source-bucket soft deletion protects accidental deletion.
- A daily scheduled function copies every authorized source generation to an independently permissioned private recovery bucket for 35 days.
The first two layers provide fast operational recovery. The third reduces the blast radius of active-bucket cleanup, account deletion, and compromised application identities. The development recovery bucket remains in the same project and therefore does not protect against whole-project deletion. Before production promotion, provision its recovery bucket in the separately administered project/bucket approved in 376-Q1.
Authority and object layout
Only an image satisfying all three conditions is protected:
- canonical path
users/{uid}/recipes/{recipeId}/{objectId}; recipeIdresolves to a recipe in a recipe box owned byuid; and- that recipe's exact
image_urlresolves to the same object path.
creator_uid, sharing membership, object metadata, and an arbitrary users/
path are not ownership proof. Protected copies preserve trusted validation
metadata, but the Firestore relationship and canonical path remain authority.
Recovery objects use this immutable, generation-addressed layout:
recipe-image-generations/{sourceObjectPath}/generations/{sourceGeneration}
Each object records ownerUid, recipeId, sourceBucket, sourceObject,
sourceGeneration, source creation/update time, contentType, contentHash,
and protectedAt. A rerun skips an existing destination generation.
Temporal selection
For a selected Firestore backup, choose the newest valid protected image
generation whose source creation/update timestamp is at or before that
Firestore recovery point. Never silently substitute a later image. A missing
qualifying generation or mismatched Firestore image_url is a recovery gap and
must be reported.
Versioned generation copying avoids requiring every unchanged image to be duplicated every day: one immutable generation can serve multiple daily or weekly Firestore recovery points. Thirty-five-day artifact retention covers at least seven daily and four weekly selection points while #377 finalizes the coordinated Firestore schedule.
Excluded and exceptional references
- External and local references are reported, not copied.
images/place-holder.pngis reproducible deployment content, not user data.- Missing, legacy, malformed, cross-tenant, mismatched, and orphaned references use the classifications in the #374 scope and are never guessed or remapped.
- Recovery artifacts are not touched by account deletion because they reside in a separate bucket to which the account-deletion identity has no access.
IAM and privacy
myrecipes-image-backup is the dedicated development runtime identity. It can
read source objects, create/read recovery objects, inspect the recovery bucket,
and write content-free status documents. It cannot delete recovery artifacts.
The Cloud Scheduler service agent can mint an OIDC token only for this runtime
identity. The recovery bucket uses uniform bucket-level access and is not
available through client Firebase configuration or Storage rules.
Monitoring
The daily protection function records counts, classifications, generation counts, duration, bucket names, and timestamps without logging recipe content or image bytes. A six-hour watchdog checks bucket access and the latest success. It fails after 30 hours. Copy and watchdog failures feed a log-based metric and Cloud Monitoring policy routed to the private owner/operator email channel.
Cost estimate
As of August 2026, regional Standard Storage in us-east1 is approximately
$0.020 per GB-month, Class A operations approximately $0.005 per 1,000, and
Class B operations approximately $0.0004 per 1,000. Same-region Google Cloud
data transfer is not charged. Cloud Run functions and operations may fall
within billing-account free tiers, but the estimate must not assume that.
Approximate monthly storage cost is:
average protected generation GB × $0.020
Operation cost is dominated by one daily versioned listing, metadata reads, and one Class A copy only when a new generation appears. At the development exercise size (seven copied generations totaling 476 bytes), storage and operation cost rounds to less than $0.01/month. Production cost scales with image bytes changed during the 35-day window, not total daily snapshots of unchanged images. Recalculate from production inventory before promotion.
Development validation
On August 20, 2026 in myrecipes-test:
- the production-equivalent scheduled job protected all four initial fixture generations and reported zero missing authorized paths;
- a canonical fixture was deleted and restored from its recovery copy;
- the fixture was overwritten and restored from the selected prior generation;
- exact owner, recipe, source path/generation, content type, content hash, and Firestore relationship checks passed;
- unauthenticated access to a protected object returned HTTP 403; and
- the watchdog produced a failure at 31 hours, feeding the configured alert, after which a successful protection run restored healthy state.
Production configuration must be promoted separately after review. No production object, bucket, function, schedule, or policy was modified or tested by this exercise.