Skip to main content

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

TokenSourcePurposeLifetime
pushTokenDevice registrationStandard push notifications (not LA-specific)Until app uninstall or OS rotation
pushToStartTokenActivityKitAllows backend to remotely start a Live ActivityUntil app uninstall; registered once
updateTokenPer-activity pushTokenUpdatesUpdate or end a specific Live Activity instanceTied to the LA instance lifecycle

Token rotation

iOS can rotate the updateToken for an active Live Activity at any time. The flow:
  1. iOS fires pushTokenUpdates on the Activity object
  2. TokenManager observes the new token, deduplicates (skips if same as current)
  3. Posts to POST /matches/:matchId/activity-token with the new token
  4. 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:
StateBell iconMeaning
Not followingOutline bellTap to follow
Following, LA activeFilled bellMatch followed, LA running
Following, LA disabledFilled bell + warningMatch followed, but no LA permission
PendingLoading indicatorFollow/unfollow in progress

DynamoDB schema

live-activities table

AttributeTypeDescription
matchId (PK)StringThe match being followed
userId#deviceId (SK)StringComposite key for user + device
statusStringpending, active, or ended
updateTokenStringCurrent APNs update token for this LA
endReasonStringmatch_end, user_unfollow, user_dismissed, token_invalid
createdAtNumberEpoch timestamp
ttlNumberAuto-cleanup after match ends

active-matches table

AttributeTypeDescription
matchId (PK)StringThe match being polled
previousScoresMapLast known scores per innings (for Tier0 detection)
cooldownsMapEvent type → expiry timestamp
lastBallsListLast 30 balls (for Tier1 detection)
pollerStateStringpolling, paused, ended
lastUpdatedAtNumberEpoch timestamp