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