How it works
When a user taps Follow on a match, the iOS app callsPOST /user/follow/match with the matchId and an optional teamId (for team-centric perspective).
The backend creates a record in the cricket-match-follows table. This record has a TTL of 48 hours after the match ends, so stale follows are automatically cleaned up by DynamoDB.
Live Activity registration
If the match is currently live, the app immediately starts a Live Activity via ActivityKit. This is an async process:Request Live Activity
iOS calls
ActivityKit.request() to start a new Live Activity on the Lock Screen and Dynamic Island.Observe push token
The app observes the
pushTokenUpdates async stream on the Activity. Apple assigns a unique push token specifically for this Live Activity instance.Register token with backend
Once the token arrives, iOS sends it to the backend to register in
cricket-activity-subscriptions.Team perspective switching
Users can change which team’s perspective they see via:teamId on the follow record. The next time MatchDisplayService formats a push payload for this user, it uses the new team perspective. The Live Activity itself does not need to be recreated.
Unfollowing
When a user unfollows:- The follow record is deleted from
cricket-match-follows - The activity subscription is removed from
cricket-activity-subscriptions - The iOS app ends the Live Activity locally
Stale subscription recovery
Key tables
| Table | Purpose |
|---|---|
cricket-match-follows | Tracks which users follow which matches (TTL: 48h post-match) |
cricket-activity-subscriptions | Maps activity push tokens to users and matches |
Key endpoints
| Method | Path | Purpose |
|---|---|---|
| POST | /user/follow/match | Follow a match |
| DELETE | /user/follow/match/:id | Unfollow a match |
| PATCH | /user/follow/match/:id/team | Switch team perspective |
Areas of improvement
1. Race condition: follow check-then-insert is not atomic
2. Unfollow is not transactional across stores
In theDELETE /user/follow/match/:id handler, unfollowMatch() and removeSubscriptionsForUser() are called sequentially without a transaction. If the process crashes or the second call fails, the follow record is deleted but orphaned activity subscriptions remain in cricket-activity-subscriptions. These subscriptions will receive pushes for a user who has unfollowed.
DynamoDB
TransactWriteItems cannot be used here because removeSubscriptionsForUser performs a query-then-delete pattern (unknown number of items). A pragmatic fix is to reverse the order — delete subscriptions first, then the follow record — so a retry of the unfollow still cleans up correctly. The existing TTL on subscriptions provides a safety net, but the window can be up to 48 hours.3. Orphaned follow state after iOS crash
If the iOS app crashes afterpostFollow succeeds but before followedMatchIds.insert() and persistState() execute, the backend has a follow record but the client has no local knowledge of it. On next launch, restoreExistingActivities() only recovers activities that ActivityKit still has in memory — it does not reconcile with the backend. There is no “GET my follows” endpoint the client can call to resync.
4. No retry on activity token registration failure
InLiveActivityService.swift, postActivityToken(matchId:pushToken:) logs the error but does not retry. If this POST fails (network blip, server error), the backend never learns about the Live Activity push token and the user receives no score updates despite having an active Live Activity on their Lock Screen. The pushTokenUpdates stream only fires when the token changes, so there is no natural retry trigger.
A simple fix is to store the pending token locally and retry on a timer or on next app foreground. The push-to-start token registration (
postPushToStartToken) has the same issue.5. Stale activity check incorrectly ends activities during rain delays
6. TTL calculation uses match start time, not end time
InmatchFollowStore.ts, the TTL is calculated as matchStartTime + 48 hours. For multi-day Test matches (which can last 5 days), the follow record could expire via DynamoDB TTL while the match is still in progress. The comment says “48 hours covers two full days of play plus rain delays / super overs” but this is insufficient for Test cricket.
Consider recalculating the TTL when the match transitions to
completed, or using a longer buffer for multi-day formats. Alternatively, the live polling loop could extend TTLs for matches still in progress.7. No offline follow queue on iOS
BothfollowMatch overloads in LiveActivityService.swift call the backend first and return nil on failure with no retry mechanism. If the user taps Follow while offline, the action silently fails. The pendingUnfollows pattern shows that offline retry was implemented for unfollows but not for follows.
Implement a symmetric
pendingFollows queue persisted to UserDefaults, retried on connectivity restoration or next app foreground, matching the existing pendingUnfollows pattern.8. Backend does not end the Live Activity on unfollow
TheDELETE /user/follow/match/:id handler removes the follow record and activity subscriptions but does not send an APNs end event to dismiss the Live Activity. It relies entirely on the iOS client calling endLiveActivity(matchId:) locally. If the client never receives the HTTP response (network timeout), the Live Activity stays visible on the Lock Screen indefinitely (until the stale check fires after 30 minutes).
Sending an APNs Live Activity update with
"event": "end" from the backend on unfollow would provide server-authoritative dismissal, removing the dependency on client-side cleanup.9. removeSubscriptionsForUser issues unbounded parallel deletes
In activitySubscriptionStore.ts, removeSubscriptionsForUser queries all subscriptions for a match filtered by userId, then fires Promise.all on every delete. For a user with many devices or token rotations, this could hit DynamoDB throughput limits. Unlike removeAllSubscriptionsForMatch (which uses batchDeleteByPartitionKey with retry and backoff), this function has no throttling.
Consider reusing
batchDeleteByPartitionKey or at minimum chunking the deletes into batches of 25 (DynamoDB BatchWriteItem limit) with backoff.