/verify skill. They are the guardrails that keep the codebase consistent across the backend, iOS app, widgets, and infrastructure.
Server-driven UI
Server-driven UI
The single most important architectural rule in the project.
Violations (flag these in any iOS code)
- String formatting or number formatting of data (e.g., building
"87/6"from separate runs/wickets values, computing run rates, formatting overs) - Conditional logic based on match state (e.g., checking innings number to decide what text to show, computing progress from overs)
- Hardcoded display strings that should come from the backend (e.g., match titles, toss descriptions, status messages)
- Data transformations or computations on backend-provided values
- Parsing backend strings to extract sub-values (e.g., splitting overs
"14.2"to compute ball number)
Allowed client-side logic (pure view concerns)
- Color/theming decisions (mapping a sentiment string to a
Color) - Simple array filtering on a boolean flag (e.g.,
isOnStrike) - Emoji flag maps (Live Activities cannot load network images)
- SwiftUI layout, animation, and styling code
- Local UI state (sheet presentation, scroll position, toggle state)
Backend TypeScript
Backend TypeScript
Source of truth
- Zod schemas are the source of truth for types. Update schemas first, then code.
- Use
z.infer<typeof Schema>for TypeScript types — never duplicate type definitions. buildDisplayState.tsis the orchestrator that computes all display values.- APNs content state must mirror iOS
LiveActivityModels.swiftexactly.
Code comments
Bad examples:// Get all devices for this user, // Return the result, // Check if nullDynamoDB anti-patterns
Every scan should either:- Be replaced with a
QueryCommandon the existing key schema, OR - Justify itself with a GSI TODO comment explaining the planned index and why it hasn’t been added yet
Copy text convention
All user-facing text (notification titles, event descriptions, display labels, placeholders) MUST live in*.messages.ts files — not inline in logic files.Does NOT apply to: log messages, developer errors, schema keys, test fixtures.
iOS Swift
iOS Swift
Apple/SwiftUI best practices
- Prefer
.foregroundStyle()over deprecated.foregroundColor() - Prefer
.task {}over.onAppear { Task {} }for async work - Use
@ViewBuilderfor functions returning conditional views - Prefer
letovervar— mutability only when required - Flag
print()statements — useos.Loggeror remove before merge
Access control
- Use
privatefor properties/methods not needed outside the type - Exception: a top-level type that is the sole primary declaration in its own file should be
internal(default), neverprivate—privateat file scope makes it invisible to the rest of the target
View rules
- Views should be small and focused — extract sub-views for complex layouts
- Avoid heavy computation in
body— use computed properties or cache - No I/O operations in views (should be in Services)
- Use
Color("colorName")for asset catalog colors or Theme constants
Model rules
- Models are
Codablestructs that mirror backend JSON exactly - Conform to
Codable,Hashable, andSendable - Use
letfor all properties — models are immutable data containers - No business logic or computed display values in models
- Both Sightscreen and SightscreenWidget targets share identical model files — flag any divergence
Theme rules
- Use
enumwithstatic letfor namespaced constants - Flag hardcoded color/spacing values in other files that should use Theme constants
Paging pattern
UseScrollView(.horizontal) + .scrollTargetBehavior(.paging) for paged views. Never use TabView(.page) — it has a known Apple bug with misaligned pages when content sizes differ. See ADR-010.iOS Services — DI pattern
iOS Services — DI pattern
Required shape for every service
- Protocol:
@MainActor protocol FooService: AnyObject, Observable— the interface - Real implementation:
@Observable @MainActor final class RealFooService: FooService— production impl - Fake implementation:
@Observable @MainActor final class FakeFooService: FooService— preview/test stub - EnvironmentKey:
defaultValueset toFakeFooService()so previews work with zero boilerplate - EnvironmentValues extension: exposing
var fooService: any FooService
File organization
- Protocol file contains: protocol +
EnvironmentKey+EnvironmentValuesextension - Real impl lives in its own file (e.g.,
CognitoAuthService.swift) - Fake impl lives under
Services/Fake/(e.g.,Fake/FakeAuthService.swift)
Consumption
- Views consume via
@Environment(\.fooService) var fooService - App root injects real impl via
.environment(\.fooService, realFooService) - Previews get the fake default automatically — no explicit environment wiring needed
- When a service holds a reference to another service, type it as
any FooService(existential), never a concrete class
Additional rules
- Services handle networking, persistence, and system integrations
- Services must NOT contain display formatting logic — that belongs on the backend
- Use
asyncfunctions, never blocking calls - Handle errors gracefully and propagate via
throws - No SwiftUI imports in services (exception:
EnvironmentKey/EnvironmentValuesdeclarations andScenePhasereferences)
Live Activity rules
Live Activity rules
ActivityAttributes vs ContentState
Push token lifecycle
- Never read
.pushTokensynchronously after creation — it will benil. Must observeactivity.pushTokenUpdates(AsyncSequence) withfor awaitto get the initial token and handle system-initiated token rotations. - When the system rotates a push token, send the new token to the backend and invalidate the old one immediately.
All 6 presentations are mandatory
EveryActivityConfiguration MUST implement:compactLeading— must deep-link to the same destination as compact trailingcompactTrailing— must deep-link to the same destination as compact leadingminimal— must show live data (score, overs), never just a static logoexpanded— full Dynamic Island expanded view- Lock Screen closure
- Watch/CarPlay via
.supplementalActivityFamilies([.small])
Dynamic Island layout
- Background is ALWAYS black and not customizable — use bold foreground colors for brand identity
- Use
ContainerRelativeShapefor inner rounded rectangles (concentric corner radii) - Do not add padding between content and the TrueDepth camera in compact views
- Use
.numericTextcontent transition for counting numbers (scores, overs) - Animation max duration is 2 seconds (system-enforced)
Lock Screen layout
- Use 14pt standard margins; no hardcoded fixed frames
- Use
activityBackgroundTint(_:)for custom background colors - Use
activitySystemActionForegroundColor(_:)for the dismiss button — verify in both light and dark mode - Check
isLuminanceReducedfor Always-On displays — no animations play on Always-On displays
StandBy mode
- StandBy scales the Lock Screen presentation 2x — all image assets must be high-resolution enough
- The system extends the background color to fill the entire screen in StandBy
- Graphic elements drawn to edges will be clipped — use dividers or containing shapes at boundaries
- Verify contrast under StandBy Night Mode (automatic red tint in low light)
Watch and CarPlay
- Use
@Environment(\.activityFamily)and switch on.smallto provide the Watch/CarPlay layout
- Apple Watch: use at most ONE interactive control — multiple controls are unusable at Smart Stack sizes
- Tapping a Watch Live Activity navigates straight to the Watch app — no expanded view exists
Lifecycle management
- Live Activities can ONLY be requested when the app is in the foreground, after a discrete user action
- Must handle all four activity states:
.active,.stale,.ended,.dismissedviaactivityStateUpdates - When
.dismissed, stop tracking and clean up — failing to observe dismissal leads to zombie state - Provide a meaningful final
ContentStatewhen callingend()— show the final match result - Set a custom
dismissalPolicywhen ending — 15-30 minutes after completion for cricket - Check
ActivityAuthorizationInfo.frequentPushesEnabledonce after activity start and send to the backend - System enforces a max 8-hour duration — implement restart logic for longer cricket matches
NSSupportsLiveActivitiesFrequentUpdatesmust beYESin Info.plist- Alert
titleandbodydisplay ONLY on a paired Apple Watch, not on iPhone
Multi-activity behavior
- When multiple Live Activities are active, the system shows the
minimalpresentation - Use
relevanceScoreto influence which activity the system prioritizes
APNs contract
APNs contract
Payload rules
Required headers and fields
apns-push-typemust beliveactivityapns-topicmust be<bundle-id>.push-type.liveactivity(not the bare bundle ID)- Must use token-based APNs connections — certificate-based connections do not support the
liveactivitypush type - Every update payload MUST include
timestamp(Unix epoch seconds),event("update"or"end"), andcontent-state - If
timestampis older than the last processed update, the system silently discards it
Priority
- Default to
apns-priority: 5(low — delivered opportunistically, no rate limit) - Use
10(high — immediate delivery) only for genuinely time-sensitive moments like wickets or score changes - Over-using priority 10 exhausts the device budget and triggers throttling
No double notifications
Ending via push
- Set
eventto"end"and includedismissal-date(Unix epoch) to control how long the ended activity stays on the Lock Screen - Omitting
dismissal-datelets the system decide (up to 4 hours) — for cricket, 15-30 minutes is appropriate
Optional fields
stale-date(Unix epoch): if set, iOS marks the activity as stale after this time without a newer update. Only use if the widget checkscontext.isStale.relevance-score(number): controls ordering when multiple activities are active. Set higher values for the match the user is most likely watching.- Push tokens are per-activity and rotate. When the iOS app sends a new token, invalidate the old one immediately in the backend token store.
Widget rules
Widget rules
File organization (one view per file)
SightscreenWidgetLiveActivity.swiftis the widget entry point ONLY — containsWidgetconformance andActivityConfigurationbuilder- All view structs it references live in separate files under
LiveActivity/
Directory layout
Styling
- Use
WidgetColorsconstants (e.g.,Color.ssAmber,Color.ssFooterGradient1) instead of inline hex colors - Widget views receive ALL display data via
LiveActivityDisplayStatefrom APNs push updates — never compute or derive display values
Live Activities cannot load network images — emoji flag maps are acceptable.
Access control for extracted views
- Top-level types in their own file MUST use
internalaccess (Swift default — no keyword). Do NOT mark themprivate. - Properties and methods that are implementation details within a view should still be
private.
Preview conventions
- Each view file SHOULD include its own
#Previewmacros at the bottom - Preview sample data lives in
Preview Content/LiveActivityPreviewData.swift, NOT inline in view files - Sample data properties use
internalaccess so all view files can reference them
Schema rules
Schema rules
- Schemas define the contract between backend and iOS
- Any schema change requires a matching update in iOS models
- Use
.nullable()for optional API fields,.optional()for include-dependent fields - Add inline comments for non-obvious fields (units, ranges, format examples)
Version safety net
If
isLiveStatus, isCompletedStatus, formatScoreDisplay, formatOversDisplay, or getLiveNote return values change in matchDisplay.ts, also verify MATCH_SCHEMA_VERSION bump if the response shape is affected.Copy and tone
Copy and tone
The Sightscreen voice: informed cricket fan, not commentator.
Tone
- Write like a fan watching with mates, not a broadcaster performing
- Understated and dry over loud and hype — “in a hurry” not “ON FIRE”
- Let rare moments (hat-trick, century) earn their excitement; routine events stay calm
- No adjective inflation: avoid “magnificent”, “brilliant”, “incredible”, “explosive”
- If the moment is big, the data says it — the copy doesn’t need to
Punctuation
- Reserve
!for genuinely rare events (hat-tricks, centuries, consecutive sixes) - Flag any line with two or more
! - Use em dash
---consistently as the separator between description and stats - No code-style formatting in user-facing strings (
`,~, etc.)
Brevity and clarity
- Copy should be scannable at lock-screen glance speed
- Avoid redundancy between title and description — don’t repeat what’s already obvious
- No commentary-box cliches: “brings up”, “reaches a milestone”, “dispatches to the boundary”
- Prefer real cricket vernacular over generic sports hype (“spell” not “on fire”, “departs” not “is out”)
Infrastructure scripts
Infrastructure scripts
Safety defaults
Required behavior
- Must support
--sandboxflag to target sandbox tables (prefixcricket-sandbox-) vs production (prefixcricket-) - Must check AWS credentials before any operations (
aws sts get-caller-identity) - Must be idempotent — if a table already exists, skip creation gracefully
- Use
aws dynamodb wait table-existsafter creating tables - Use
PAY_PER_REQUESTbilling mode (no capacity planning needed) - Print summary at the end with table names and required
.envadditions - File naming:
migrate-NNN-description.sh(sequential numbering)
GSI documentation
When adding a GSI, document why it’s needed in a comment. When usingScanCommand in the corresponding code, document why Scan is acceptable (e.g., small table size, infrequent access, cached result).Architecture decisions
Architecture decisions
If a PR introduces a significant architectural change (new infrastructure pattern, protocol change, dependency strategy, data model redesign, etc.), it should be documented as an Architecture Decision Record in
docs/architecture/adr/.If the change is urgent and an ADR is not feasible, at minimum create a GitHub issue documenting the decision and link it in the PR.See the full ADR catalog for all recorded decisions.