LA start: two triggers
A Live Activity can be started by two paths. Both converge on the same token registration flow.
Trigger 1: User follows a live match
Trigger 2: Orchestrator detects match going live
MatchOrchestrator runs a 30-second polling loop. It detects matches transitioning to live status and handles startup recovery — if the backend restarts while matches are live, the Orchestrator picks them up on the next cycle and creates any missing PENDING records.
LA update
Once a Live Activity is active (has a registered update token), it receives score updates through the event-driven pipeline.
Every follower of the same match gets the identical payload. The UnifiedDisplayState is built once per match, not per user. See Live score pipeline for the full detection and payload construction flow.
LA end: four reasons
A Live Activity can end for four distinct reasons. Each has a different trigger path.
1. Match ends
2. User unfollows
3. User dismisses LA on device
Dismissing the LA on-device triggers an unfollow. This is intentional — the LA is the primary UI for a followed live match. If the user dismisses it, the backend treats that as “I don’t want updates for this match.”
4. Token becomes invalid
Token invalidation is self-healing. The Orchestrator’s 30-second loop detects followers who should have an active LA but don’t, and re-sends push-to-start. This handles device restarts, OS-terminated activities, and token rotation.
Token management
Three token types
| Token | Source | Purpose | Lifetime |
|---|
| pushToken | Device registration | Standard push notifications (not LA-specific) | Until app uninstall or OS rotation |
| pushToStartToken | ActivityKit | Allows backend to remotely start a Live Activity | Until app uninstall; registered once |
| updateToken | Per-activity pushTokenUpdates | Update or end a specific Live Activity instance | Tied to the LA instance lifecycle |
Token rotation
iOS can rotate the updateToken for an active Live Activity at any time. The flow:
- iOS fires
pushTokenUpdates on the Activity object
TokenManager observes the new token, deduplicates (skips if same as current)
- Posts to
POST /matches/:matchId/activity-token with the new token
- Backend updates the
live-activities record in-place (same PK/SK, new token value)
If the token POST fails (network blip), the backend has a stale token. The next push attempt will get a 410 from APNs, triggering the token-invalid recovery flow. There is retry logic in TokenManager, but a persistent network outage will eventually lead to re-push-to-start via the Orchestrator.
Multi-device support
A single user can have active LAs on multiple devices simultaneously. The live-activities table uses userId#deviceId as the sort key, so each device gets its own record. When the LACoordinator fans out pushes, it sends to all active tokens for a match — regardless of how many belong to the same user.
iOS service architecture
The old monolithic LiveActivityService has been split into three focused managers:
TokenManager
Handles all token observation and registration.
- Observes
pushToStartToken changes on app launch
- Observes
pushTokenUpdates for each active Live Activity
- Deduplicates tokens (skips POST if token hasn’t changed)
- Retries failed token registrations
LifecycleManager
Manages the Activity object lifecycle.
- Calls
Activity.request() to create new LAs
- Restores existing activities on app relaunch (checks ActivityKit state)
- Runs a stale check: if no update received in 30 minutes, ends the LA locally
- Adopts push-started activities (activities created via push-to-start that need local tracking)
FollowManager
Manages the follow/unfollow user intent and local state.
- Calls
POST /user/follow/match and DELETE /user/follow/match
- Persists followed match IDs in
UserDefaults for instant UI state
- Retries pending unfollows on next app launch (handles offline unfollow)
- Drives the bell icon states:
| State | Bell icon | Meaning |
|---|
| Not following | Outline bell | Tap to follow |
| Following, LA active | Filled bell | Match followed, LA running |
| Following, LA disabled | Filled bell + warning | Match followed, but no LA permission |
| Pending | Loading indicator | Follow/unfollow in progress |
DynamoDB schema
live-activities table
| Attribute | Type | Description |
|---|
matchId (PK) | String | The match being followed |
userId#deviceId (SK) | String | Composite key for user + device |
status | String | pending, active, or ended |
updateToken | String | Current APNs update token for this LA |
endReason | String | match_end, user_unfollow, user_dismissed, token_invalid |
createdAt | Number | Epoch timestamp |
ttl | Number | Auto-cleanup after match ends |
active-matches table
| Attribute | Type | Description |
|---|
matchId (PK) | String | The match being polled |
previousScores | Map | Last known scores per innings (for Tier0 detection) |
cooldowns | Map | Event type → expiry timestamp |
lastBalls | List | Last 30 balls (for Tier1 detection) |
pollerState | String | polling, paused, ended |
lastUpdatedAt | Number | Epoch timestamp |
Related pages