Skip to content

MyRecipes 1.0 entitlement-operation matrix

Issue: #303

Schema: users/{uid}.subscription.schemaVersion = 1

Authority: verified store lifecycle reconciliation; client booleans are never authoritative

Baseline: 2026-08-02

State decisions

Canonical state Cloud reads and export Cloud creates/updates/uploads Delete and account cleanup Purchase recovery
trial Allowed Allowed until expiresDateMs Allowed Allowed
active Allowed Allowed until expiresDateMs Allowed Allowed
grace Allowed Allowed until expiresDateMs Allowed Allowed
outage Allowed Allowed until server-issued outageEndsAtMs (at most 72 hours) Allowed Allowed
migration Allowed Allowed until approved migrationEndsAtMs Allowed Allowed
retry / recovery Allowed Denied Allowed Allowed
expired Allowed during the approved retention period Denied Allowed Allowed
revoked / refunded Allowed during the approved retention period Denied immediately after trusted confirmation Allowed Allowed
Missing, malformed, or unknown Only where ownership rules independently permit it Denied Allowed where ownership rules independently permit it Allowed

Every write decision also requires schemaVersion == 1; legacy isActive, isSubscribed, cached UI state, and caller-supplied account IDs are ignored. On sign-out/account deletion, the client terminates Firestore and clears its persistent cache before another UID can initialize; in-memory recipe and entitlement providers also reset on account change.

Operation enforcement

Operation Enforcement path Lifecycle-race behavior Limit / control
Recipe-box create App Check callable + backend transaction Entitlement is read in the resource transaction 25/account; callable, daily quota, kill switch
Recipe save, edit, import, copy, and recipe children App Check callable + backend transaction Entitlement is read in the same transaction as recipe/child replacement Recipe/child ceilings; import and daily quotas; kill switch
Recipe-box and legacy recipe/child direct update Firestore Rules Rules evaluate canonical entitlement at commit time Existing document only; creates remain backend-only
Recipe and recipe-box delete Firestore Rules Remains available after entitlement loss Reduces retained state
Recipe image authorization App Check callable + backend transaction Entitlement is read in the reservation transaction 5 MiB/object, 2 GiB/account, upload/daily quota, kill switch
Recipe image upload Storage Rules + exact backend reservation Canonical entitlement is checked again at upload time; a reservation cannot survive revocation JPEG/PNG, exact bytes/type, ten-minute reservation
Recipe image delete Storage Rules Remains available after entitlement loss Owner only
Shopping-list/item/aisle-map/reusable-item create App Check callable + backend transaction Entitlement is read in the resource transaction Per-account/list ceilings; callable, daily quota, kill switch
Shopping rename, item toggle/edit, aisle propagation, reusable-item edit Firestore Rules Canonical entitlement is evaluated when an online or queued offline write reaches Firestore Existing document only
Shopping deletes and member removal Firestore Rules or cleanup callable Remain available after entitlement loss Reduces state/access
Add recipe-box or shopping-list member App Check callable + backend transaction Entitlement is re-read in the membership transaction 10 members/resource and shared-resource ceilings
Remove member App Check callable Remains available after entitlement loss Reduces access
Authentication, offer display, restore/reconcile, export, support, account deletion Dedicated minimum/recovery paths Does not depend on write entitlement Rate limiting and ownership checks where applicable

Offline and outage behavior

Reads may use Firestore's existing local cache. A direct update queued while offline is not accepted merely because the UI previously showed paid access: Firestore evaluates the current server-owned entitlement when the write reaches the service. If the account expired or was revoked meanwhile, the write fails with permission-denied; the existing local data is retained for recovery.

Store-verification failure does not accept a client assertion. Only the backend may issue outage with the approved bounded deadline. When that deadline ends, cloud writes fail closed until verified reconciliation restores access.

Denial telemetry and customer errors

Callable denials emit structured Cloud Logging event entitlement_write_denied with operation, canonical state, and schema version; no receipt, email, transaction ID, or raw entitlement payload is logged. Backend errors use permission-denied, reason=write-entitlement-required, and include the operation. Rules and App Check denials are monitored through Firebase Rules, App Check, and callable metrics. Alerts and cost controls are owned by #276.

The app must treat permission-denied on a cloud mutation as a recoverable access decision: keep local content, refresh entitlement, and offer subscription management or restore. Limit failures use resource-exhausted; temporary kill switches use unavailable and do not imply data loss.

Residual exposure and rollout

Some high-frequency edits remain direct to preserve the approved Option B offline design. They are protected by App Check deployment controls, ownership, canonical entitlement, existing-document-only rules, and schema validation, but they do not pass through the callable per-minute/per-day counters. Abnormal direct-write volume therefore remains a monitoring and emergency-rules/kill- switch concern; it is not eliminated by subscription enforcement alone.

Deployment order is mandatory:

  1. Deploy lifecycle reconciliation and migrate eligible existing accounts to a valid schema-v1 entitlement (or an explicitly dated migration state).
  2. Deploy functions with transaction-coupled entitlement checks and confirm denial telemetry.
  3. Deploy Firestore and Storage Rules; unknown or unmigrated states fail closed.
  4. Run emulator state coverage and production canaries for active, grace, expired, revoked, outage, delete, upload, App Check, and account switching.
  5. Release the matching client binary. Roll back the binary/functions/rules as a versioned set; do not weaken rules to accept client subscription booleans.