assignDailyQuestion and aggregateOutcomeStats previously did an unbounded
db.collection('couples').get() (loading every couple into memory), and aggregate did a
serial outcomes.get() per couple (O(couples) round-trips). Both now paginate the couple
scan (orderBy __name__ + startAfter, 300/200 per page — no custom index needed):
- assignDailyQuestion: each page's create() writes fan out with the burst bounded to a page
instead of all couples at once; ALREADY_EXISTS stays the idempotent no-op.
- aggregateOutcomeStats: reads each page's outcomes in parallel instead of serially.
couples.length still counts every couple, so the aggregate windows + totalCouples are
unchanged (pure aggregate() helper and its tests untouched).
This is the one behavior-touching improvement flagged in the plan; the 512MiB/300s resource
options from B2 remain the safety net. Build clean; 70 tests green. dist rebuilt.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Migrate all callables off functions.https.onCall to firebase-functions/v2/https onCall:
createInvite, acceptInvite, leaveCouple, submitOutcome, sendGentleReminder,
sendThinkingOfYou, checkDeviceIntegrity, syncEntitlement, assignDailyQuestionCallable,
wrapReleaseKey. context.auth/app → request.auth/app, data arg → request.data. The 8
client-hardcoded callable names are preserved verbatim (verified via emulator discovery).
The manual `if (!request.app)` App Check check is a 1:1 port (no enforceAppCheck switch).
Hardening folded in:
- acceptInviteCallable: await the previously fire-and-forget partner_joined push (gen 2
freezes the instance after the response) — still swallows push errors so a failed push
never fails the accept.
- checkDeviceIntegrity: 10s timeout on the Play Integrity client.request so a hung upstream
can't pin the instance (fail-closed catch already handles the throw); memory 512MiB.
- wrapReleaseKey: memory 512MiB (tink); HttpsError swapped to v2; lazy tink require + graceful
failure preserved.
- Error mapping with `if (e instanceof HttpsError) throw e` re-throw guard around the risky
DB sections in acceptInvite, leaveCouple, submitOutcome, sendGentleReminder,
sendThinkingOfYou — raw errors map to a clean 'internal' without masking intentional codes
(resource-exhausted rate limits, permission-denied, etc.). leaveCouple's best-effort
recursiveDelete sweep now swallows errors (the transactional leave already succeeded).
- Adopt shared sendPushToUser()/logger; remove copied token readers + plaintext token logging.
Delete dead placeholder callables notifications/reminders.ts (sendDailyQuestionReminder,
sendPartnerAnsweredNotification) — no client caller; wrote sent:false rows nothing consumed.
Build clean; 70 tests green; discovery loads all callables as v2 in us-central1. dist rebuilt.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Migrate all scheduled jobs off functions.pubsub.schedule().onRun() to
firebase-functions/v2/scheduler onSchedule({ schedule, timeZone, ...opts }, handler):
sendChallengeDayReminders, unlockDueMemoryCapsules, sendDailyQuestionProactiveReminder,
sendStreakReminder, sendReengagementReminder, assignDailyQuestion (scheduled export),
aggregateOutcomeStats, scheduledOutcomesReminder.
Hardening folded in:
- Fan-out isolation: Promise.all → Promise.allSettled in dailyQuestionReminder (outer+inner),
reengagement, gameRetention (both jobs), scheduledOutcomesReminder — one bad couple can no
longer abort a whole run. streakReminder / assignDailyQuestion already isolated.
- Resource options: assignDailyQuestion + aggregateOutcomeStats memory 512MiB + timeout 300s
(they iterate all couples); the four fan-out reminders get timeout 180s.
- Adopt shared sendPushToUser()/logger everywhere; remove five copied getUserTokens() and the
copied send/prune blocks (no plaintext token logging remains here).
- Consolidate duplicated date/time helpers into notifications/time.ts (chicagoDateKey, toMillis),
replacing streakReminder's + scheduledOutcomesReminder's per-file copies.
assignDailyQuestion.ts callable export stays v1 for now (migrates in B3); its tested CST helpers
are untouched. Scanner pagination for assignDailyQuestion/aggregateOutcomes is deferred to B6.
Build clean; 70 tests green (tested pure helpers preserved). dist rebuilt. Still on v5.1.1.
Co-Authored-By: Claude Opus 4.8 <[email protected]>