docs(manual): Batch 4 — update Cloud Functions list, module responsibilities, webhook ack order, and schedule timezones

This commit is contained in:
null 2026-06-28 11:07:24 -05:00
parent 08368b3e01
commit d19f0f8eb8
2 changed files with 21 additions and 18 deletions

View File

@ -77,8 +77,8 @@ collection name, and architectural fact. Never assume.
|---|---|---|---|
| 1 | ✅ done | `core/feature/` note inaccurate (dir doesn't exist); `data/questions/` listed `QuestionDao` but it's in `data/local/` | Corrected `core/feature/` note; moved `QuestionDao` to `data/local/`; kept `QuestionJsonParser` in `data/questions/`; updated older-description note. |
| 2 | ✅ done | No anonymous sign-in or account linking in code; Android uses legacy Google Sign-In SDK (idToken), not Credential Manager; `encryptionMigrationUsers` field does not exist | Removed anonymous auth and account-linking claims; corrected Google Sign-In description; removed `encryptionMigrationUsers` from couples model and added note that `encryptionVersion` is always `2`. |
| 3 | todo | | |
| 4 | todo | | |
| 3 | ✅ done | `/users/{uid}` model missing `sex`, `partnerId`, `plan`, `lastActiveAt`, notification prefs, quiet-hours, `fcmToken`; `hasPremium` is not a real root field (premium lives in `/entitlements/premium`); `/couples/{coupleId}` listed non-existent `encryptionMigrationUsers`; date plan fields wrong; date plan preference fields wrong; bucket list fields wrong; missing `/answers/{userId}/secure/{doc}` for schemaVersion 2 | Updated `/users/{uid}` to full allowlist; removed `hasPremium` root field and added note about `/entitlements/premium`; removed `encryptionMigrationUsers`; corrected date plan, preference, and bucket list fields; added `secure` subdoc to daily-question model. |
| 4 | in progress | | |
| 5 | todo | | |
| 6 | todo | | |
| 7 | todo | | |

View File

@ -754,39 +754,42 @@ Every function module follows the same shape:
| Type | Example | Notes |
| --- | --- | --- |
| HTTPS onRequest | `revenueCatWebhook`, `health` | Path-based; bypass callable auth. Webhook requires Ed25519 signature verification. |
| HTTPS onRequest | `revenueCatWebhook` | Path-based; bypass callable auth. Webhook requires Ed25519 signature verification. The unauthenticated `health` endpoint was removed in a security review. |
| HTTPS onCall | `createInviteCallable`, `acceptInviteCallable`, `syncEntitlement`, `sendDailyQuestionReminder`, `sendPartnerAnsweredNotification`, `sendGentleReminderCallable`, `submitOutcomeCallable`, `leaveCoupleCallable`, `checkDeviceIntegrity`, `assignDailyQuestionCallable` | Caller must be authenticated. Errors throw `HttpsError`. |
| Firestore onCreate | `onAnswerWritten`, `onMessageWritten`, `onCoupleLeave`, `onUserDelete`, `onGameSessionUpdate`, `createDateMatchOnMutualLove` | Event-driven; best-effort. |
| Firestore onCreate | `onAnswerWritten`, `onAnswerRevealed`, `onMessageWritten`, `onCoupleLeave`, `onUserDelete`, `onGameSessionUpdate`, `onGamePartFinished`, `notifyOnDateMatch` | Event-driven; best-effort. |
| Firestore onUpdate | `onEntitlementChanged` | Watches entitlement doc and mirrors premium state to the user root doc for clients that read `plan`. |
| Auth onDelete | `onUserDelete` | Auth user deletion cascade. |
| Pub/Sub schedule | `assignDailyQuestion`, `scheduledOutcomesReminder` | Cron expression in `America/Chicago`. |
| Pub/Sub schedule | `assignDailyQuestion`, `scheduledOutcomesReminder`, `sendDailyQuestionProactiveReminder`, `sendReengagementReminder`, `unlockDueMemoryCapsules`, `sendChallengeDayReminders` | Cron expression; timezone is `America/Chicago` only where explicitly set. |
### Per-module responsibilities
- **billing** — RevenueCat webhook, entitlement event handlers, forced re-sync callable. Entitlement writes are idempotent (write the same Firestore doc and use `entitlement_events/` as a dedup marker).
- **couples** — invite create/accept/leave, outcome submission, scheduled 30/60/90 reminders.
- **dates** — mutual-love trigger creates a date match document.
- **games** — game session updates notify the partner and append to `notification_queue`.
- **notifications** — daily question reminders, partner-answered notifications, gentle reminders, challenge day reminders, capsule unlock schedule.
- **questions** — daily question assignment, answer write trigger, thread message trigger.
- **security** — Play Integrity verdict verification.
- **users** — Auth user deletion cascade.
- **billing** — RevenueCat webhook, entitlement event handlers (`onEntitlementChanged`), forced re-sync callable (`syncEntitlement`). Entitlement writes are idempotent (write the same Firestore doc and use `entitlement_events/` as a dedup marker).
- **couples** — invite create/accept/leave (`createInviteCallable`, `acceptInviteCallable`, `leaveCoupleCallable`, `onCoupleLeave`), outcome submission (`submitOutcomeCallable`), scheduled 30/60/90 reminders (`scheduledOutcomesReminder`).
- **dates** — mutual-love trigger creates a date match document and notifies the couple (`notifyOnDateMatch`).
- **games** — game session updates notify the partner and append to `notification_queue` (`onGameSessionUpdate`, `onGamePartFinished`).
- **notifications** — daily question proactive reminder (`sendDailyQuestionProactiveReminder`), re-engagement (`sendReengagementReminder`), gentle reminder callable (`sendGentleReminderCallable`), challenge day reminders (`sendChallengeDayReminders`), capsule unlock (`unlockDueMemoryCapsules`), placeholder callables (`sendDailyQuestionReminder`, `sendPartnerAnsweredNotification`).
- **questions** — daily question assignment (`assignDailyQuestion`, `assignDailyQuestionCallable`), answer write trigger (`onAnswerWritten`), reveal trigger (`onAnswerRevealed`), thread message trigger (`onMessageWritten`).
- **security** — Play Integrity verdict verification (`checkDeviceIntegrity`).
- **users** — Auth user deletion cascade (`onUserDelete`).
### Webhook reliability
`revenueCatWebhook` acknowledges with HTTP 200 immediately after signature verification and parses the event, then **before** applying the entitlement write. This is intentional to prevent RevenueCat retries. If `applyEntitlementEvent` fails after the 200, the failure is logged but the event is not retried. The webhook handler does not currently use a dead-letter queue. **Risk**: a transient Firestore outage could lose entitlement events. The mitigation today is `entitlement_events/{eventId}` as an idempotency marker — re-running the webhook for a missing event would dedup on event ID. A future fix should add a Cloud Tasks-based retry or a dead-letter `entitlement_events_failed/` collection.
`revenueCatWebhook` processes the entitlement write (`applyEntitlementEvent`) **before** returning the HTTP 200 acknowledgement. If processing fails, the function returns HTTP 500 so RevenueCat will retry. This changed from an earlier implementation that acked 200 immediately and silently dropped failures. `applyEntitlementEvent` is idempotent, so retries are safe. `entitlement_events/{eventId}` remains the idempotency marker.
The webhook handler does not currently use a dead-letter queue. **Risk**: repeated 500s could still lose events if RevenueCat gives up. The mitigation today is the idempotency marker — re-running the webhook for a missing event would dedup on event ID. A future fix should add a Cloud Tasks-based retry or a dead-letter `entitlement_events_failed/` collection.
### Schedule
```text
assignDailyQuestion 0 23 * * * America/Chicago (11 PM UTC; 6 PM Chicago during CDT, 5 PM during CST — see Date math DST bug)
scheduledOutcomesReminder every 24 hours America/Chicago
scheduledOutcomesReminder every 24 hours (default UTC)
sendDailyQuestionProactiveReminder 0 16 * * * America/Chicago (4 PM; 2h before expiry)
sendReengagementReminder 0 12 * * * America/Chicago (noon; targets couples 310 days quiet)
unlockDueMemoryCapsules every 1 hours (gameRetention.ts)
sendChallengeDayReminders every 24 hours (gameRetention.ts)
unlockDueMemoryCapsules every 1 hours (default UTC)
sendChallengeDayReminders every 24 hours (default UTC)
```
`scheduledOutcomesReminder` currently scans all couples with no pagination. It will need to shard or paginate as the user base grows. `sendDailyQuestionProactiveReminder` is the only deployed reminder for unanswered daily questions — the placeholder `sendDailyQuestionReminder` callable in `reminders.ts` is not in the live reminder path.
`scheduledOutcomesReminder` currently scans all couples with a 200-doc limit and no pagination. It will need to shard or paginate as the user base grows. `sendDailyQuestionProactiveReminder` is the only deployed scheduled reminder for unanswered daily questions — the placeholder `sendDailyQuestionReminder` callable in `reminders.ts` is not in the live reminder path.
---