> ## Documentation Index
> Fetch the complete documentation index at: https://285e39fd5e337e58f16290.sightscreen.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Live Activity lifecycle

> How Live Activities are created, updated, and ended — from user follow to lock screen dismissal.

## 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

```mermaid theme={null}
sequenceDiagram
    participant User as User (iOS)
    participant FM as FollowManager
    participant API as Backend API
    participant DB as DynamoDB (live-activities)
    participant APNs as ApnsDeliveryService
    participant iOS as iOS (ActivityKit)
    participant TM as TokenManager
    participant LM as LifecycleManager

    User->>FM: Tap Follow on live match
    FM->>API: POST /user/follow/match {matchId, deviceId}
    API->>DB: Create LA record (status=pending)
    API->>APNs: Send push-to-start to device(s)
    APNs->>iOS: Push-to-start notification
    iOS->>LM: ActivityKit creates Live Activity
    LM->>LM: Activity.request() with initial state
    iOS->>TM: pushTokenUpdates fires
    TM->>TM: Deduplicate token
    TM->>API: POST /matches/:matchId/activity-token {token, deviceId}
    API->>DB: Update record: status=active, set updateToken
    API-->>TM: 200 OK
    Note over API,iOS: LA is now receiving score updates
```

### Trigger 2: Orchestrator detects match going live

```mermaid theme={null}
sequenceDiagram
    participant MO as MatchOrchestrator (30s loop)
    participant SM as SportMonks
    participant DB as DynamoDB (live-activities)
    participant APNs as ApnsDeliveryService
    participant iOS as iOS (ActivityKit)
    participant TM as TokenManager
    participant API as Backend API

    MO->>SM: Check match status
    SM-->>MO: Match is now LIVE
    MO->>MO: Query followers for this match
    MO->>DB: Create PENDING LA records for each follower+device
    MO->>APNs: Send push-to-start to all follower devices
    APNs->>iOS: Push-to-start notification
    iOS->>iOS: ActivityKit creates Live Activity
    iOS->>TM: pushTokenUpdates fires
    TM->>API: POST /matches/:matchId/activity-token {token, deviceId}
    API->>DB: Update: status=active, set updateToken
    Note over MO,iOS: Orchestrator also handles startup recovery (missed matches)
```

<Note>
  **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.
</Note>

## LA update

Once a Live Activity is active (has a registered update token), it receives score updates through the event-driven pipeline.

```mermaid theme={null}
sequenceDiagram
    participant EB as MatchEventBus
    participant LAC as LACoordinator
    participant DB as DynamoDB (live-activities)
    participant APNs as ApnsDeliveryService
    participant Apple as Apple APNs
    participant iOS as iOS Device

    EB->>LAC: Emit MatchBusEvent (displayState + keyEvent)
    LAC->>DB: Query: matchId, status=active
    DB-->>LAC: Active LA records with tokens
    LAC->>LAC: Build APNs payload from UnifiedDisplayState
    LAC->>LAC: Set priority: P10 (alert-worthy) or P5 (silent)
    LAC->>APNs: sendBatch(tokens, payload)
    APNs->>Apple: HTTP/2 push per token
    Apple-->>iOS: Push notification
    iOS->>iOS: ActivityKit updates content state
    iOS->>iOS: Widget re-renders Lock Screen + Dynamic Island
```

<Tip>
  Every follower of the same match gets the identical payload. The `UnifiedDisplayState` is built once per match, not per user. See [Live score pipeline](/business-flows/live-scores) for the full detection and payload construction flow.
</Tip>

## LA end: four reasons

A Live Activity can end for four distinct reasons. Each has a different trigger path.

### 1. Match ends

```mermaid theme={null}
sequenceDiagram
    participant EB as MatchEventBus
    participant LAC as LACoordinator
    participant DB as DynamoDB (live-activities)
    participant APNs as ApnsDeliveryService
    participant iOS as iOS Device

    EB->>LAC: MatchBusEvent with keyEvent=match_end
    LAC->>DB: Query all active records for matchId
    LAC->>APNs: Send event="end" with dismissal-date
    APNs->>iOS: End Live Activity push
    iOS->>iOS: LA shows final score, auto-dismisses after delay
    LAC->>DB: Mark all records: status=ended, endReason=match_end
```

### 2. User unfollows

```mermaid theme={null}
sequenceDiagram
    participant User as User (iOS)
    participant FM as FollowManager
    participant API as Backend API
    participant DB as DynamoDB (live-activities)
    participant APNs as ApnsDeliveryService
    participant iOS as iOS Device

    User->>FM: Tap Unfollow
    FM->>API: DELETE /user/follow/match {matchId}
    API->>DB: Query all LA records for userId + matchId
    API->>APNs: Send event="end" to all user's devices
    APNs->>iOS: End Live Activity push
    iOS->>iOS: LA dismissed
    API->>DB: Mark records: status=ended, endReason=user_unfollow
    API-->>FM: 200 OK
    FM->>FM: Clear local state (UserDefaults)
```

### 3. User dismisses LA on device

```mermaid theme={null}
sequenceDiagram
    participant User as User (iOS)
    participant iOS as iOS (ActivityKit)
    participant LM as LifecycleManager
    participant API as Backend API
    participant DB as DynamoDB (live-activities)

    User->>iOS: Swipe to dismiss Live Activity
    iOS->>LM: Activity state → dismissed
    LM->>API: POST /matches/:matchId/activity-dismissed {deviceId}
    API->>API: Unfollow match for user
    API->>DB: Mark record: status=ended, endReason=user_dismissed
    API-->>LM: 200 OK
```

<Warning>
  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."
</Warning>

### 4. Token becomes invalid

```mermaid theme={null}
sequenceDiagram
    participant LAC as LACoordinator
    participant APNs as ApnsDeliveryService
    participant Apple as Apple APNs
    participant DB as DynamoDB (live-activities)
    participant MO as MatchOrchestrator

    LAC->>APNs: sendBatch includes stale token
    APNs->>Apple: HTTP/2 push
    Apple-->>APNs: 410 Gone (token invalid)
    APNs-->>LAC: Invalid token response
    LAC->>DB: Mark: status=ended, endReason=token_invalid

    Note over MO,DB: Next Orchestrator cycle
    MO->>MO: Detect follower with no active LA
    MO->>APNs: Re-send push-to-start
    Note over MO,DB: Token flow restarts (new LA created)
```

<Note>
  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.
</Note>

## 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:

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)

<Warning>
  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.
</Warning>

### 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

* [Live score pipeline](/business-flows/live-scores) -- how scores flow from SportMonks through the EventBus to APNs
* [Match following](/business-flows/match-following) -- the user action that triggers LA creation
* [Device registration](/business-flows/device-registration) -- push token types and multi-device support
