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

# Match following

> What happens when a user follows a match — from tap to Live Activity.

```mermaid theme={null}
sequenceDiagram
    participant User as User (iOS)
    participant FM as FollowManager
    participant API as Backend API
    participant MF as DynamoDB (match-follows)
    participant LA as DynamoDB (live-activities)
    participant APNs as ApnsDeliveryService
    participant iOS as iOS (ActivityKit)
    participant TM as TokenManager

    User->>FM: Taps Follow on match
    FM->>FM: Persist matchId to UserDefaults (instant UI update)
    FM->>API: POST /user/follow/match {matchId, deviceId}
    API->>MF: Create match-follow record (TTL: 48h post-match)

    alt Match is LIVE
        API-->>FM: 200 OK {displayState (cached)}
        API->>LA: Create PENDING LA record
        API->>APNs: Send push-to-start to device
        APNs->>iOS: Push-to-start notification
        iOS->>iOS: ActivityKit creates Live Activity with initial displayState
        iOS->>TM: pushTokenUpdates fires
        TM->>API: POST /matches/:matchId/activity-token {token, deviceId}
        API->>LA: Update: status=active, set updateToken
    else Match is UPCOMING
        API-->>FM: 200 OK
        Note over API,iOS: No LA created yet. Orchestrator will start it when match goes live.
    end
```

## How it works

When a user taps **Follow**, the `FollowManager` on iOS drives the flow.

<Steps>
  <Step title="Local state first">
    `FollowManager` immediately writes the matchId to `UserDefaults`. This makes the bell icon update instantly -- no waiting for the network round-trip.
  </Step>

  <Step title="API call">
    `POST /user/follow/match` with `matchId` and `deviceId` (`identifierForVendor`). The backend creates a record in `match-follows` with a TTL of 48 hours after the match ends.
  </Step>

  <Step title="If match is live: push-to-start">
    The backend returns the cached `displayState` so iOS can show initial data immediately. Simultaneously, it creates a PENDING record in `live-activities` and sends a push-to-start notification to the device.
  </Step>

  <Step title="LA creation on device">
    iOS receives the push-to-start and ActivityKit creates the Live Activity on the Lock Screen and Dynamic Island. If the user has LA permissions disabled, the follow still works (bell icon shows a warning badge) but no LA appears.
  </Step>

  <Step title="Token registration">
    Once the LA is created, iOS observes `pushTokenUpdates` on the Activity. `TokenManager` sends the token to `POST /matches/:matchId/activity-token`. The backend activates the LA record.
  </Step>

  <Step title="Score updates begin">
    The `LACoordinator` now includes this token when fanning out score updates for the match. See [live score pipeline](/business-flows/live-scores).
  </Step>
</Steps>

### When LA permissions are disabled

If the user has denied Live Activity permissions (or the device doesn't support them), the follow is still created on the backend. The user is "following" the match -- they just won't get a Lock Screen widget. The bell icon shows a warning state to communicate this.

The user can still see match updates by opening the app.

## Unfollowing

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

    User->>FM: Taps Unfollow
    FM->>FM: Remove matchId from UserDefaults
    FM->>API: DELETE /user/follow/match {matchId}
    API->>MF: Delete match-follow record
    API->>LA: Query all LA records for userId + matchId
    API->>APNs: Send event="end" to all user's devices
    APNs->>iOS: End Live Activity
    iOS->>iOS: LA dismissed from Lock Screen
    API->>LA: Mark all records: status=ended, endReason=user_unfollow
    API-->>FM: 200 OK
```

Key points about unfollow:

* The backend sends an APNs `end` event to **all devices** for this user+match. Multi-device cleanup is server-authoritative.
* `FollowManager` removes the matchId from `UserDefaults` immediately (optimistic UI).
* If the DELETE request fails (offline), `FollowManager` queues it as a pending unfollow and retries on next app launch.

<Warning>
  If the user dismisses the Live Activity directly on the Lock Screen (swipe to dismiss), iOS notifies `LifecycleManager`, which calls `POST /matches/:matchId/activity-dismissed`. The backend treats this as an unfollow. See [LA lifecycle - user dismiss](/business-flows/live-activity-lifecycle#3-user-dismisses-la-on-device).
</Warning>

## Syncing follows on login

On login, the iOS app calls `GET /user/follows` to fetch the list of active follow records from the backend. This reconciles local state with server truth and handles:

* Follows created on another device
* Local state lost due to app reinstall
* Follows that expired while the user was signed out

## Key tables

| Table             | Key schema                           | Purpose                                                       |
| ----------------- | ------------------------------------ | ------------------------------------------------------------- |
| `match-follows`   | PK: `userId`, SK: `matchId`          | Tracks which users follow which matches (TTL: 48h post-match) |
| `live-activities` | PK: `matchId`, SK: `userId#deviceId` | Maps active LA tokens to users and devices                    |

## Key endpoints

| Method | Path                                   | Purpose                                 |
| ------ | -------------------------------------- | --------------------------------------- |
| POST   | `/user/follow/match`                   | Follow a match                          |
| DELETE | `/user/follow/match`                   | Unfollow a match                        |
| GET    | `/user/follows`                        | List followed match IDs (sync on login) |
| POST   | `/matches/:matchId/activity-token`     | Register LA update token                |
| POST   | `/matches/:matchId/activity-dismissed` | User dismissed LA (triggers unfollow)   |

## Related pages

* [Live Activity lifecycle](/business-flows/live-activity-lifecycle) -- full LA creation, update, and end flows
* [Live score pipeline](/business-flows/live-scores) -- what happens after the LA is active and receiving updates
* [Device registration](/business-flows/device-registration) -- push tokens and multi-device support
