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.
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
| 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 |
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.
Running the generator
# 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 |
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.
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 |
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.
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)"
)
}
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.
| 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 |
Most transformations are handled automatically by CodingKeys enums. Only use custom init(from:) implementations for genuinely complex mappings.