Recoverable user-backup replacement commit strategy
Issue: #381
Environment for implementation and acceptance: myrecipes-test only
Decision date: 2026-08-21
This note is the required design checkpoint before implementing full replacement. It defines how a confirmed #379 archive replaces one authenticated user's in-scope recipe dataset without treating Firestore and Cloud Storage as one transaction. Production promotion is separate and remains gated by #391.
Chosen activation strategy
Version 1 uses a journaled multi-stage commit with a backend-enforced owner lease. It does not add a dataset-generation pointer to every application query.
The current global collections and numeric business IDs cannot be switched with one per-user pointer. A delete-then-recreate workflow would therefore be unrecoverable. Replacement instead uses these protections:
- lock the affected owner's recipe reads and writes;
- produce and independently validate a seven-day safety export;
- stage and verify the complete incoming graph and image bytes privately;
- journal the exact active documents and quarantine image content;
- activate in bounded idempotent batches;
- verify the active dataset before reporting success; and
- retain enough private material for a verified rollback for seven days.
Partial active data is never reported as successful. During active mutation, the affected owner's recipe dataset is temporarily unavailable to the owner and collaborators. Out-of-scope application features remain available.
Authoritative boundary
The included and excluded collections remain those in
docs/release/recipe-backup-scope.md. The authenticated UID and
trusted backend mappings establish ownership. Archive IDs establish
relationships only; archive ownership fields, paths, URLs, bucket names, and
membership claims never establish authority.
| Phase | Authoritative artifact | Purpose |
|---|---|---|
| Before mutation | Locked active dataset plus verified safety export | Recovery point if staging or activation fails |
| Staging | Validated archive generation/hash plus private staged graph | Candidate dataset; never client-readable |
| Committing | Exact rollback journal/quarantine plus operation batch ledger | Resume or restore after interruption |
| Verifying | Active graph plus immutable operation expectations | Prove counts, ownership, relationships, and images |
| Completed | Verified active graph | Application source of truth |
| Rollback | Exact journal/quarantine | Restore the pre-import active state |
The confirmed upload remains immutable by object generation and SHA-256. The safety export and rollback journal are independent artifacts; neither is discarded merely because the other exists.
Operation state machine
The existing #379 import operation transitions from confirmed into the #381
states below. Transitions require the expected prior state and record a trusted
server timestamp. Publication and rollback publication use the specifically
defined atomic multi-document batches rather than a separate state transition.
confirmed
|
v
safety_export --> failed (no active mutation)
|
v
staging ------> failed (no active mutation)
|
v
committing ---------> rollback_required
| |
v v
verifying ----------> rolling_back
|
v
publishing ------------> rollback_required
|
v
completed
rolling_back -> verifying_rollback -> publishing_rollback -> rolled_back
safety_export: lease acquired; internal complete export is being created and revalidated.staging: trusted IDs and paths are assigned; documents and images are private and are verified against the validated archive.committing: an active-mutation marker and exact journal exist; bounded batches are journaled and idempotently applied.verifying: all expected active writes completed; final graph/image checks run while every replacement read block remains enabled. It never reports success or clears maintenance state.publishing: verification succeeded; one atomic Firestore batch is making the complete verified dataset readable and marking the operation complete.completed: publication succeeded and the verified dataset is readable. Only deferred cleanup may remain.failed: terminal pre-mutation failure; active content was never changed.rollback_required: mutation may have occurred and normal access stays locked until resume or rollback succeeds.rolling_back: deterministic document/image restoration is running. Every original recipe-box document is restored with its original application fields, but its replacement projection is deliberately overridden to remain blocked for the current operation.verifying_rollback: the complete restored logical state is checked while the deliberate replacement-projection overrides keep access blocked.publishing_rollback: one atomic batch reopens all restored boxes and marks the operation rolled back.rolled_back: exact pre-import active state was restored and verified.
An operation also stores a monotonic phase sequence, current batch number, completed batch keys, heartbeat, cleanup status, bounded error code, and counts. Retries compare these fields before doing work. Duplicate confirmation or task delivery returns the existing operation and never creates another dataset.
Lease ownership and access enforcement
The authoritative lease document is
user_recipe_replacement_locks/{ownerUid}. Lease ownership is the tuple
ownerUid + operationId + approved replacement service identity; it never
belongs to one worker invocation. It contains the operation ID, phase sequence,
acquired/renewed/expiry timestamps, active-mutation marker, latest heartbeat,
service identity, and schema version.
- Lease duration: 30 minutes.
- Worker heartbeat: at least every two minutes.
- Any worker using the approved service identity may renew it after validating owner UID, operation ID, current state, expected phase sequence, active-mutation status, and latest lease metadata.
- Another import cannot take over an expired lease.
- A pre-mutation stale lease may be released only after proving no active mutation occurred, no active-mutation marker exists, rollback is unnecessary, and staging cleanup is safe.
- A post-mutation stale lease transitions to
rollback_required; access does not reopen automatically.
Expected operational phase ceilings are 15 minutes for safety export, 30 minutes for staging/journaling, 30 minutes for committing, 15 minutes for verification, five minutes for publication, 30 minutes for rollback, and five minutes for rollback publication. Heartbeat absence for five minutes alerts the operator. Any post-mutation lock older than 45 minutes creates a high-priority recovery incident. The escalation path is: inspect operation and batch ledgers, stop duplicate dispatch, invoke deterministic resume when its preconditions hold, otherwise invoke the restricted rollback function, and record the reason/result in the administrative audit. Expiry never reopens post-mutation access.
Emulator-backed read-lock feasibility result
The design does not add another lease-document lookup to every rule. Cloud Storage recipe-image authorization already reads the recipe and box documents; adding a third Firestore access would exceed the safe established shape. The authoritative lease is instead projected by trusted backend code to:
users/{ownerUid}.recipe_replacement_read_blocked; and- every affected
recipeboxes/{boxId}.replacement_read_blocked.
Both projections also carry the operation ID. They remain true through
committing, verifying, and rollback_required, even if the lease timestamp
expires. New active boxes are created blocked and are unblocked only after final
verification. Firestore recipe/child rules use the already-read box record;
Storage uses its existing recipe-plus-box reads. Direct staging/journal paths
remain default-deny.
The proof is implemented by
functions/src/recipe_replacement_lock_poc.test.ts with service-only rules in
functions/test-fixtures/. npm run test:replacement-lock-poc passes nine
emulator tests and proves:
- current owned/shared box and recipe query shapes succeed while unlocked;
- owner, collaborator, child, and image reads fail while the owner is blocked;
- the owner can read the intentional maintenance marker;
- a shared-box query constrained by
replacement_read_blocked == falseexcludes a locked owner's boxes while returning unrelated unlocked owners; - the subsequent realistic recipe query succeeds with ten distinct unlocked boxes, the existing Firestore rule-access ceiling;
- an outdated mixed recipe query that includes a locked box fails closed;
- stale post-mutation projection remains blocking; and
- all staging/journal Storage and Firestore namespaces deny client access.
The application must add the unlocked constraint to owned/shared box queries,
hydrate recipes only from the returned unlocked box IDs, and render the owner's
maintenance marker intentionally. Before rollout, existing box records require
replacement_read_blocked: false plus the corresponding composite indexes.
Modified or outdated clients cannot read partial data: a direct locked document
read fails, and a mixed query containing a locked result is rejected wholesale.
Projection occurs before activeMutationStartedAt. It may use multiple bounded
batches because active content has not changed. Before mutation, a fresh check
must prove the user marker and every affected current box carry true and the
current operation ID; every staged future box is configured to be created
blocked; direct affected document/image reads fail; recipe writes and membership
changes fail; and the journal, quarantine, and staged candidate are complete and
verified. Interrupted projection either finishes idempotently or removes only
the current operation's projections and leaves the original dataset unchanged.
Both the incoming normalized archive and the user's current owned dataset must contain no more than 25 recipe boxes before mutation begins. An account exceeding that limit must be rejected before block projection or handled by a separately approved migration.
During committing, verifying, rollback_required, rolling_back, and
verifying_rollback, reads of every in-scope collection that may be partially
replaced are denied by Firestore rules. Recipe boxes, recipes,
recipe_ingredients, recipe_directions, recipe_notes,
private_recipe_notes, and recipe_categories follow the projected box
relationship. recipe_user_metadata, recipe_user_measures, and
recipe_user_ingredients also consult the existing owner user-document
maintenance projection. No in-scope collection in recipe-backup-scope.md is
excluded from this guarantee.
A modified or outdated client therefore cannot use a direct auxiliary-document read to observe partial replacement data. Normal reads resume only after atomic publication or rollback publication clears the projections. Direct user measure/ingredient reads add one user-document rules access. User metadata requires at most the user, recipe, and box documents (three accesses for a direct read); private notes use the recipe and box relationship (two). Emulator tests cover every in-scope auxiliary collection while unlocked, blocked, and republished and must remain within Firestore's access-call limits.
Cost/performance impact is bounded: box queries use the resource field and add no document access; recipe/child queries retain their existing box lookup; Storage retains its existing two Firestore accesses; the owner maintenance preflight adds one user-document read per hydration cycle. Query chunks remain at no more than ten distinct box IDs.
The following paths must check the owner lease:
| Path | Enforcement |
|---|---|
| Recipe-box create/update/delete | Callable transaction and Firestore rules |
| Recipe and child save/delete | Callable transaction and Firestore rules |
| User recipe measures/ingredients/metadata reads and writes | Owner maintenance projection, relationship-aware Firestore rules, and callable transaction |
| Image reserve/upload/finalize/delete | Callable transaction and Storage rules |
| Recipe-box membership mutation | Callable transaction and Firestore rules |
| Administrative recipe migrations | Explicit backend lease preflight |
| Owner/collaborator in-scope reads during mutation and rollback verification | Trusted projected block, Firestore/Storage rules, filtered queries, and maintenance UI |
Shopping lists, profiles/Auth, entitlements, purchases, usage/rate-limit data, and administrative audit data are outside the lock and replacement boundary.
Projection fields are server-owned. Firestore rules allow clients to read the owner maintenance status but require ordinary user updates to preserve these fields exactly:
users/{ownerUid}.recipe_replacement_read_blocked
users/{ownerUid}.recipe_replacement_operation_id
recipeboxes/{boxId}.replacement_read_blocked
recipeboxes/{boxId}.replacement_operation_id
Only myrecipes-user-replacement@${projectId}.iam.gserviceaccount.com may change
them. Ordinary box creation runs through trusted code and writes the normal
false value with no operation ID. Owners, collaborators, anonymous/unrelated
users, old clients, migrations, and administrative helpers cannot clear or
replace projections; non-replacement trusted tools must explicitly preserve
them. Rules tests exercise all client roles. Backend integration tests exercise
administrative preservation and the replacement identity allowlist.
Firestore IAM is not field-granular: any project principal with broad Datastore write authority is privileged at the platform layer. Deployment therefore limits deliberate projection mutation to replacement functions running as the approved identity, removes that identity from unrelated functions, inventories every other privileged runtime/tool, and requires their code and tests to preserve projections. Client rules provide field-level enforcement for SDK clients; IAM, function deployment review, and audit alerts provide the trusted backend boundary.
Safety-export gate
The replacement worker creates an internal #378-format export after acquiring the lease. This export does not consume the user's ordinary export/recovery allowance. It must be:
complete, neverincomplete;- bound to the locked owner;
- checksum-valid and revalidated with the released import validator;
- immutable by recorded object generation and SHA-256;
- count-matched to the locked active inventory; and
- retained privately for seven days.
The operation records the safety-export operation ID, generation, hash, counts, creation time, and expiry. Staging may begin after this gate, but active mutation may not begin until both the safety export and staged candidate are verified.
Physically isolated staging and trusted identity mapping
Firestore staging uses exactly:
user_backup_replacement_staging/{operationId}
user_backup_replacement_staging/{operationId}/entries/{sequenceId}
user_backup_replacement_staging/{operationId}/image_entries/{imageId}
The parent is a manifest; each entry is one bounded normalized future active document with collection, trusted document/business IDs, canonical payload checksum, ordering key, and owner/operation identity. It is never stored in an active global collection.
The dedicated private bucket ${projectId}-user-replacement uses:
staging/{ownerUid}/{operationId}/images/{archiveImageId}.{jpg|png}
safety-exports/{ownerUid}/{operationId}/archive.zip
journals/{ownerUid}/{operationId}/firestore/chunk-{sequence}.ndjson
quarantine/{ownerUid}/{operationId}/images/{encodedLogicalPath}
Firebase Storage rules deny all client access to the bucket/prefixes. Only
myrecipes-user-replacement@${projectId}.iam.gserviceaccount.com and the
separately documented cleanup identity receive least-privilege object access.
Firestore rules deny all client access to staging, journal manifests, locks,
batch ledgers, and replacement audit records.
On activation, trusted workers read staged entries and write newly mapped active
documents in bounded batches; images are copied to controlled canonical active
paths with generation preconditions. Pre-mutation failure deletes staging after
verification. Completion/rollback marks it cleanup-eligible; a scheduled sweep
finds expired manifests by expiresAt, verifies no rollback dependency, deletes
bounded pages/prefixes, and records completion.
Trusted code allocates new Firestore document IDs, numeric business IDs,
canonical image paths, and object metadata. It creates explicit maps for boxes,
recipes, children, ingredient parents, catalog rows, and images. It preserves
valid portable content timestamps and records restoredAt separately.
Before activation, verification proves:
- expected document and image counts;
- authenticated ownership on every staged root;
- every recipe-to-box and child-to-recipe relationship;
- every ingredient parent relationship within the same recipe;
- unique controlled IDs and paths;
- image length, SHA-256, MIME type, extension, and byte signature; and
- absence of out-of-scope fields and archive authority.
Version 1 sharing policy
Every imported recipe box is private. Names and archive-local IDs never establish box identity, so v1 performs no name matching and transfers no membership. The archive cannot create collaborators. Current owned boxes absent from the archive are removed, and their existing collaborator access ends. Users may share imported boxes again after completion.
Before confirmation, preview and confirmation both display:
Imported recipe boxes will be private. Existing sharing and collaborator access for recipe boxes being replaced will be removed. You can share the imported boxes again after the import finishes.
Memberships on boxes owned by other users remain unchanged. Those boxes, recipes, images, and personal metadata are outside staging/journaling. Tests assert no write targets them; semantic snapshots, document/business IDs, membership/access relationships, and out-of-scope sentinels remain equal. Their Storage paths, generations, hashes, metadata, and bytes remain unchanged.
Scalable journal and journaled activation
Before the first active mutation, trusted code creates and verifies:
- a complete checksummed journal in bounded Cloud Storage chunks;
- a private quarantine copy of each current owned image with source generation, path, metadata, size, and SHA-256; and
- a deterministic activation plan divided into bounded batches.
The Firestore manifest is
user_backup_replacement_journals/{operationId}. Journal objects are immutable
NDJSON chunks containing at most 500 entries and at most 4 MiB uncompressed.
Each entry contains schema/sequence, original collection/document identity,
original business identifiers, complete original field/timestamp values,
membership state, owner/operation identity, and a canonical SHA-256. The
manifest records schema version, expected entry/chunk counts, ordered chunk
paths/generations/lengths/checksums, creation status, and trusted completion
timestamp.
Chunk creation is resumable by deterministic sequence. Verification rereads
every immutable chunk by generation, validates entry/chunk checksums and order,
and compares expected counts with a fresh locked inventory. A manifest written
before a missing chunk remains incomplete. activeMutationStartedAt cannot be
set until independent verification marks the manifest complete and all image
quarantine copies are verified.
The operation then records activeMutationStartedAt before applying batch one.
Firestore activation and rollback batches use at most 450 deterministic content
mutations. The corresponding batch-ledger completed write and verification
metadata are included in the same atomic Firestore WriteBatch, remaining below
the 500-write limit. There is no separate durable claim whose existence implies
success. If a worker dies before commit, neither mutations nor completion exist;
if commit returns or is uncertain, the next worker reads the ledger and verifies
all deterministic postconditions before proceeding. Deletes cover only the
authoritative #374 selection. Writes use only staged trusted data.
Storage cannot join that batch. Each deterministic image action has explicit
pending, applying, and completed ledger state. A resumed applying action
checks source/destination generations, path, hash, length, MIME, metadata, and
signature to decide whether to complete, retry with preconditions, or require
rollback. A claim alone never proves an object mutation completed.
Firestore and Storage steps are deliberately not called atomic. The lease, journal, batch ledger, immutable object generations, and verification make interruption recoverable.
Verification and success
Success is reported only when a fresh active inventory proves:
- document counts equal the normalized staged expectations;
- ownership and all relationships are valid;
- no replaced old document remains active;
- every expected image exists at its controlled path and matches its recorded new generation, size, hash, MIME type, metadata, and signature;
- every imported box is private with an empty collaborator list;
- all excluded-data sentinels are unchanged; and
- two-user isolation sentinels are unchanged.
A complete empty archive follows the same process. It removes all current in-scope content. The application requires a default recipe box, so an archive with zero boxes is normalized to one empty private default box. Preview and confirmation disclose that normalization; expected counts include it; and the completion report identifies it. Verification states: the active dataset equals the normalized interpretation of the archive, not its literal box count.
Timestamp restoration rules
createdAtandupdatedAtpreserve portable content history when they are RFC 3339 UTC values from 2000-01-01T00:00:00Z through the manifestexportedAt, inclusive.updatedAtis preserved rather than replaced by the import time; when both exist it may not precedecreatedAt.- Missing portable timestamps remain null.
- Malformed, pre-boundary, future-after-export, and inverted timestamps are fatal validation errors; they are not silently normalized.
restoredAt, operation transitions, leases, audits, journal completion, and cleanup timestamps always use trusted server timestamps.- Ownership, entitlement, membership, purchase, usage, and administrative timestamps are outside the archive contract and are never restored.
Tests cover valid historical values, both inclusive boundaries, null/missing
values, malformed text, pre-boundary values, future-after-export values, and
updatedAt < createdAt.
Atomic publication
No imported box or image is readable before the normalized active dataset and
all canonical image relationships pass verification. With the enforced
ACCOUNT_LIMITS.recipeBoxes == 25, replacement publication uses one atomic
Firestore WriteBatch containing:
- at most 25 imported box updates clearing the read block and operation ID;
- one owner user-document update clearing its maintenance projection;
- one replacement-operation update to
completedwith trustedpublishedAt; and - one deterministic publication-ledger write containing completion metadata.
The maximum is therefore 28 writes, leaving 472 writes of headroom below Firestore's 500-write limit. The implementation uses a stricter 450-write internal ceiling. Archive validation already rejects 26 boxes; confirmation and the pre-mutation inventory gate recheck the same enforced bound. Visibility flags are never cleared in multiple publication batches.
Publication preconditions are: state verifying, current immutable verification
evidence, matching owner/operation projections on all expected boxes and user,
complete image verification, and absent/conflicting publication ledger. The
atomic batch clears every projection, records the publication ledger/timestamp,
and changes the state to completed together. Every update uses the exact
document update-time observed during the precondition read so a concurrent
change fails the entire batch.
On retry or an uncertain commit response, the worker rereads the operation, user marker, publication ledger, and every expected box marker:
- all postconditions true means publication completed;
- all preconditions still true and no publication mutation means retry the same deterministic batch;
- any impossible mixed state remains blocked and becomes
rollback_requiredor restricted administrative recovery.
An attempted batch never implies success. Duplicate delivery of a completed
operation performs no write. Publication failure is not deferred cleanup: it
cannot set completed, clear maintenance, or report success.
Atomic rollback reopening
During rolling_back, every original recipe-box document is restored with its
original logical application fields except the two projected replacement
fields, which are intentionally forced to:
replacement_read_blocked: true
replacement_operation_id: currentOperationId
Those overrides remain unchanged throughout rolling_back and
verifying_rollback. Only after complete rollback verification does
publishing_rollback use one atomic batch of the same maximum 28 writes to
clear every restored original box projection, clear the user marker, set
rolled_back, record trusted rollbackPublishedAt, and write the deterministic
rollback-publication ledger.
Uncertain rollback publication uses the same complete pre/postcondition reconciliation. Restored boxes never reopen incrementally. Any mixed or failed result remains locked for deterministic retry or restricted recovery.
Retry and rollback
Pre-mutation failures become failed, release the lease after cleanup is
verified, and leave active content unchanged. Post-mutation failures remain
locked and become rollback_required unless the same operation can safely
resume its deterministic plan.
Rollback reverses the activation plan using the journal and quarantined image
content. “Exact Firestore restoration” means exact original logical application
fields, document IDs, business IDs, memberships, and timestamps. The temporary
replacement projections are deliberately not restored from the journal during
rolling_back or verifying_rollback; they remain blocked for the current
operation until atomic rollback publication.
Rollback also restores the same logical image paths, bytes, SHA-256, required custom metadata, MIME types, and relationships. Cloud Storage normally assigns a new generation; rollback records and verifies that restored generation. Original source generations are recorded and generation-match preconditions protect the copy into quarantine. “Exact rollback” means exact logical application state, subject to those temporary projection overrides and never reuse of a provider-assigned generation number.
Automatic workers may resume the same operation or execute its predetermined rollback. Manual resolution requires an administrative recovery function, authorized operator, reason, and audit record. It cannot accept replacement content or arbitrary paths.
Failure-injection matrix
Integration tests must stop the worker immediately before and after every boundary, redeliver the task, and prove the expected result.
| Boundary | Expected result |
|---|---|
| Lease acquisition | One owner/operation wins; duplicate delivery reuses it |
| Before initial block projection | Active dataset unchanged; safe retry |
| During multi-batch pre-mutation projection | Active dataset unchanged; finish projection or safely remove it |
| After block projection verification | Mutation begins only when user and every affected box are blocked |
| Safety export creation | No mutation; retry or terminal failed |
| Safety export revalidation | No mutation; invalid export blocks activation |
| Document staging | No mutation; deterministic resume or cleanup |
| Image staging | No mutation; generation/hash mismatch blocks activation |
| Staged verification | No mutation; bounded fatal result |
| Journal creation | No mutation until complete journal is verified |
| Journal chunk interrupted | Deterministic resume; incomplete manifest cannot pass |
| Manifest before missing chunk | Verification rejects it before mutation |
| Active-mutation marker | Resume batch one or rollback; never release lock |
| Worker death after nominal batch claim | No claim implies completion; inspect atomic ledger/postconditions |
| Worker death after document mutations | Atomic content-plus-ledger batch is wholly applied or unapplied |
| Each delete/write batch | Idempotent resume or exact rollback |
| Quarantine source-generation mismatch | Copy fails before active mutation |
| Each image activation step | Generation-aware resume or exact rollback |
| Active verification | No success; repair/resume or rollback_required |
| Before replacement publication | Verified dataset remains fully blocked |
| Publication batch commit | All boxes, user marker, ledger, and operation state change atomically |
| Uncertain publication response | Verify every deterministic publication postcondition |
| Duplicate publication delivery | Return completed without another write |
| Rollback document batch | Idempotent resume until pre-state matches |
| Rollback image step | Restore logical path/content/metadata; record and verify new generation |
| Before rollback publication | Restored dataset remains fully blocked |
| Rollback publication batch | Restored boxes, user marker, ledger, and operation change atomically |
| Uncertain rollback publication | Verify every rollback-publication postcondition |
| Duplicate rollback publication | Return rolled_back without another write |
| Lease release | Only after verified completion or rollback |
| Deferred cleanup | Dataset remains completed; retry and alert cleanup |
Tests also cover double submission, repeated Cloud Tasks delivery, different workers renewing the same operation, another operation failing to renew/take over, expired pre/post-mutation leases, post-mutation escalation threshold, staging queried by a normal client, an empty archive and disclosed default-box normalization, 1,000 recipes, Unicode, maximum children/images, a same-name shared current box becoming private, realistic shared-query rule limits, and another user with reciprocal sharing.
Automated publication coverage additionally proves no imported box/image is
readable before publication, all boxes reopen together, the user marker remains
set until success, collaborator isolation, failure immediately before
publication, uncertain-result reconciliation, duplicate idempotency, the
25-box/28-write bound with 472-write headroom, rejection at 26 boxes, atomic
rollback reopening, projection-field client immutability, normal post-publication
queries, and the planned composite-index contract. Administrative projection
preservation and deployed migration/index verification remain mandatory
implementation gates in myrecipes-test.
Retention and cleanup
| Artifact | Retention |
|---|---|
| Unfinished upload/pre-commit staging | At most 24 hours |
| Verified safety export | Seven days after completion/rollback |
| Exact document rollback journal | Seven days after completion/rollback |
| Superseded image quarantine | Seven days after completion/rollback |
| Failed post-mutation material | Until verified rollback, then seven days |
| Bounded operation/audit metadata | Existing administrative retention policy |
After atomic publication, cleanup failure does not misreport the dataset as
failed. The operation is completed with cleanupPending: true; scheduled
idempotent cleanup retries and emits a content-free alert. Cleanup never removes
the last recoverable artifact early.
Entitlement and audit
A write-enabled entitlement is checked at confirmation and again before lease acquisition. After active mutation begins, entitlement expiry cannot strand a partial commit; the service identity must complete verification or rollback.
Audit fields are limited to actor UID, operation IDs, state transitions, timestamps, validator/export versions, archive/safety hashes and generations, counts, lease events, bounded error codes, cleanup status, and administrative resolution reason. Recipe content, filenames, collaborator addresses, URLs, archive bytes, tokens, and signed credentials are prohibited from logs/audit.
Development acceptance exercise
Implementation is accepted only after automated tests and an end-to-end
myrecipes-test exercise prove:
- export and record the target user's current counts and two-user sentinels;
- confirm a validated archive and observe the owner maintenance lock;
- verify the complete seven-day safety export before mutation;
- inject and recover from every state boundary in the matrix;
- complete replacement and verify normalized archive counts/relationships/images;
- prove absent old content is no longer active;
- prove no write targets the other user and semantic Firestore/Storage/access snapshots and exclusions are unchanged;
- exercise duplicate confirmation/task delivery and concurrent write denial;
- force a post-mutation failure and verify exact rollback;
- exercise a valid empty archive and default-box normalization;
- record duration, document/object operations, temporary storage, and cost;
- verify seven-day retention and cleanup/alert behavior.
No production replacement deployment or customer-data exercise is authorized by this note. The exact runtime identities, buckets, functions, rules, indexes, alerts, commands, and rollback evidence must be added to the development runbook and #391 production migration guide during implementation.