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

# Data models & code generation

> Generated Swift models from backend Zod schemas, the two-stage codegen pipeline, and the API contract.

## Code generation pipeline

All API-facing models are **auto-generated** from backend Zod schemas via a two-stage pipeline. This ensures the iOS app and backend always agree on data shapes.

### Two-stage codegen

```mermaid theme={null}
flowchart LR
    A[Backend Zod schemas] -->|"Stage 1: zod-to-json-schema"| B[JSON Schema files]
    B -->|"Stage 2: quicktype"| C[".generated.swift files"]
    C --> D[Main app target]
    C --> E[Widget target]
```

| Stage | Tool                 | Input                                | Output                  |
| ----- | -------------------- | ------------------------------------ | ----------------------- |
| 1     | `zod-to-json-schema` | Zod schema definitions in TypeScript | JSON Schema files       |
| 2     | `quicktype`          | JSON Schema files                    | Swift `Codable` structs |

<Warning>
  Never hand-edit any `.generated.swift` file. Changes will be overwritten on the next generation run. If the model needs to change, update the Zod schema in the backend and regenerate.
</Warning>

### Running the generator

```bash theme={null}
# From the backend project root
npm run generate:schemas
```

This reads the Zod schemas, converts them to JSON Schema, then runs quicktype to produce Swift files with matching `Codable` structs.

### Shared between targets

All generated files are included in both the **main app target** and the **widget target** (`SightscreenWidget`). This guarantees that the widget's Live Activity rendering uses the exact same types as the main app.

***

## Generated model files

### LiveActivityModels.generated.swift

The primary generated file for Live Activity payloads. Contains the full display state hierarchy.

| Type                       | Purpose                                                               |
| -------------------------- | --------------------------------------------------------------------- |
| `LiveActivityDisplayState` | Top-level display state for all LA presentations                      |
| `LiveActivityCompact`      | Display data for compact leading and trailing views                   |
| `LiveActivityExpanded`     | Display data for expanded Dynamic Island and Lock Screen              |
| `LiveActivityAttributes`   | **Static** data set at creation (match ID, team info) — cannot change |
| `ContentState`             | **Dynamic** data updated via APNs pushes (scores, overs, events)      |
| `MatchPhase`               | Enum: `pre`, `live`, `innings_break`, `completed`, `abandoned`        |
| `BattingPerspective`       | Which team is batting — drives display orientation                    |
| `MatchSentiment`           | Enum for match mood: `neutral`, `exciting`, `tense`, `dominant`       |

<Note>
  `LiveActivityAttributes` is set once at creation time and cannot change. `ContentState` is updated with every push. Anything that changes during a match must be in `ContentState`.
</Note>

### FollowModels.generated.swift

Models for the follow/unfollow system.

| Type                  | Purpose                                                        |
| --------------------- | -------------------------------------------------------------- |
| `FollowResponse`      | Backend response after follow/unfollow — confirms state        |
| `FollowStatus`        | Enum: `following`, `not_following`                             |
| `UserFollowsResponse` | Response from `GET /user/follows` — list of followed match IDs |

### HubAPIModels.generated.swift

Models for the main hub/schedule API responses.

| Type                  | Purpose                                              |
| --------------------- | ---------------------------------------------------- |
| `MatchItem`           | A single match with pre-formatted display fields     |
| `LeagueGroup`         | A group of matches under a league header             |
| `ScheduleResponse`    | Top-level schedule response — array of `LeagueGroup` |
| `LiveMatchesResponse` | Response for the live matches endpoint               |

***

## Hand-written models

Some models are hand-written because they contain client-side logic or do not map to a backend schema.

### Auth models

| Type      | Purpose                                                                            |
| --------- | ---------------------------------------------------------------------------------- |
| `AppUser` | The authenticated user — includes `isAdmin` flag derived from JWT `cognito:groups` |

### Core models

| Type                | Purpose                                                       |
| ------------------- | ------------------------------------------------------------- |
| `Team`              | Team identity — name, abbreviation, logo URL                  |
| `Match`             | Full match data from the API                                  |
| `MatchDisplayState` | Pre-formatted display state for a match (computed by backend) |

***

## Model conventions

All models (generated and hand-written) follow these conventions:

| Convention | Detail                                                                              |
| ---------- | ----------------------------------------------------------------------------------- |
| `Codable`  | All models conform to `Codable` for JSON serialization                              |
| `Hashable` | All models conform to `Hashable` for use in SwiftUI lists and diffing               |
| `Sendable` | All models conform to `Sendable` for safe use across Swift 6 concurrency boundaries |

```swift theme={null}
struct MatchItem: Codable, Hashable, Sendable {
    let matchId: String
    let league: String
    let homeTeam: Team
    let awayTeam: Team
    let displayState: MatchDisplayState
    let startTime: Date
}
```

***

## JSON decoding strategy

The app configures a shared `JSONDecoder` with ISO 8601 date support that handles both fractional seconds and standard formats.

```swift theme={null}
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom { decoder in
    let container = try decoder.singleValueContainer()
    let dateString = try container.decode(String.self)

    // Try ISO 8601 with fractional seconds first
    let formatterWithFraction = ISO8601DateFormatter()
    formatterWithFraction.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
    if let date = formatterWithFraction.date(from: dateString) {
        return date
    }

    // Fall back to standard ISO 8601
    let formatter = ISO8601DateFormatter()
    formatter.formatOptions = [.withInternetDateTime]
    if let date = formatter.date(from: dateString) {
        return date
    }

    throw DecodingError.dataCorruptedError(
        in: container,
        debugDescription: "Invalid date: \(dateString)"
    )
}
```

<Note>
  The dual-format decoder exists because some backend responses include fractional seconds (e.g., `2024-03-15T10:30:00.000Z`) and others do not (e.g., `2024-03-15T10:30:00Z`). Both must decode correctly.
</Note>

***

## API format notes

| Concern         | Detail                                                             |
| --------------- | ------------------------------------------------------------------ |
| Key format      | camelCase (matches Swift `Codable` defaults)                       |
| Date format     | ISO 8601 with or without fractional seconds                        |
| CodingKeys      | Used to map any snake\_case API keys to camelCase Swift properties |
| Custom decoders | Complex transformations live in `init(from:)` implementations      |

<Tip>
  Most transformations are handled automatically by `CodingKeys` enums. Only use custom `init(from:)` implementations for genuinely complex mappings.
</Tip>
