Skip to content

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:

  1. Source-bucket object versioning protects overwritten objects.
  2. Seven-day source-bucket soft deletion protects accidental deletion.
  3. 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};
  • recipeId resolves to a recipe in a recipe box owned by uid; and
  • that recipe's exact image_url resolves 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.png is 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.