docs/architecture/adr/ in the source repo and are numbered sequentially.
Backend infrastructure
ADR-001: APNs HTTP/2 Connection Pooling
ADR-001: APNs HTTP/2 Connection Pooling
| Status | Accepted |
| Date | 2026-02-16 |
http2.connect() calls with a persistent, lazily-initialized HTTP/2 session reused across all push requests. HTTP/2 natively supports multiplexed streams, which is exactly the model Apple designed APNs around. Key design choices include lazy singleton via getSession(), connection deduplication via a shared promise, auto-reconnection on close/error events, GOAWAY handling, and graceful shutdown with a 5-second force-destroy fallback.Consequences:- ~30x latency reduction for batched pushes (100 pushes: ~15s down to ~0.5s)
- Reduced resource usage — one long-lived connection instead of hundreds
- Simpler call sites —
sendPush()callers are unaware of connection management - Module-level mutable state makes unit testing harder (mitigated by
resetSession()for tests)
ADR-002: Hybrid In-Memory + DynamoDB Ball Processing State
ADR-002: Hybrid In-Memory + DynamoDB Ball Processing State
| Status | Accepted |
| Date | 2026-02-16 |
lastProcessedBallId per match. The in-memory Map serves as the primary read path (O(1)), with DynamoDB as a backup on cache miss and for persistence across restarts. Writes go to the Map synchronously and to DynamoDB as async fire-and-forget. A 7-day TTL on DynamoDB items provides self-cleaning.Consequences:- Each ball processed exactly once — the cursor guarantees no duplicates and no gaps
- Survives restarts — DynamoDB backup means deploys during live matches don’t lose progress
- Sub-millisecond reads on the hot path
- Assumes monotonically increasing ball IDs from SportMonks (historically true)
ADR-009: Cockatiel for SportMonks Polling Resilience
ADR-009: Cockatiel for SportMonks Polling Resilience
| Status | Accepted |
| Date | 2026-03-23 |
- Hanging requests are killed after 8s instead of blocking indefinitely
- Circuit breaker stops hammering a down API and auto-recovers via half-open test
- Jitter eliminates thundering herd on recovery
- Circuit state exposed via
GET /admin/polling/statusfor CloudWatch alarms - New dependency (zero-dep, 5KB), but module-level policy state is process-local
DynamoDB data model
ADR-003: Devices Table Match Following Extension
ADR-003: Devices Table Match Following Extension
| Status | Superseded by ADR-004 |
| Date | 2026-02-17 |
cricket-live-devices table with a followedMatches map attribute containing per-match data (startDate, supportedTeamId). This kept the read path to a single GetCommand.Consequences: Shipped but was superseded by ADR-004 because the hot path (“who follows match X?”) required a full table scan — the partition key was userId, not matchId.ADR-004: DynamoDB Table Consolidation — 6 Tables to 3
ADR-004: DynamoDB Table Consolidation — 6 Tables to 3
| Status | Accepted (supersedes ADR-003) |
| Date | 2026-02-19 |
cricket-devices— pure device registry (PK=userId, SK=deviceToken). Stripped of match-level concerns.cricket-match-follows— follow intent only (PK=matchId, SK=userId). One record per (match, user) with 48h TTL.cricket-activity-subscriptions— Live Activity sessions per device (PK=matchId, SK=laPushToken). Separated from follows because LA tokens are per-device while follows are per-user.
QueryCommand.Consequences:- 3 tables instead of 6 — simpler to reason about
- Hot-path optimized — toss detection and LA updates are single PK queries
- Auto-cleanup via DynamoDB TTL — no manual cleanup jobs
- Trade-off: “which matches does user X follow?” requires a scan (low-frequency, acceptable)
ADR-005: Single Device Per User
ADR-005: Single Device Per User
| Status | Accepted |
| Date | 2026-02-27 |
cricket-devices to one record per user (latest device wins). registerDevice() now cleans up old records, explicitly REMOVEs pushToStartToken when not provided, and iOS refreshes the push-to-start token before every follow call.Consequences:- Eliminates orphan records, stale tokens, and cross-device pollution
- Simpler code — all consumers work with
DeviceRecord | nullinstead of arrays - Trade-off: no iPad + iPhone concurrent Live Activities (acceptable edge case)
ADR-008: DynamoDB Migration Tracking
ADR-008: DynamoDB Migration Tracking
| Status | Accepted |
| Date | 2026-03-02 |
cricket-migrations-{env}). A TypeScript runner checks the registry, computes pending migrations, executes each shell script in order, and records the result. Forward-only (no rollback scripts) — to undo a bad migration, deploy the previous code version.Consequences:- Each environment is fully independent — no risk of sandbox state leaking into production
- Stop on first failure prevents cascading issues from dependent migrations
- Custom over off-the-shelf because existing tools (umzug, node-migrate) don’t handle bash script migrations well
ADR-011: Sparse GSI for Live Fixture Archival
ADR-011: Sparse GSI for Live Fixture Archival
| Status | Accepted |
| Date | 2026-03-24 |
/livescores. The poller never writes the final status, so fixtures freeze at whatever state the last poll captured — missing final status, stale resultText, missing winnerTeamId, and lingering replay fixtures.Decision: Add a sparse GSI (live-index) to the fixtures table. A liveUpdatedAt attribute is SET on every poller tick and REMOVED when the match is archived. Only items with liveUpdatedAt appear in the GSI. Each tick, the poller scans the GSI, diffs against current /livescores, and reconciles off-air matches by fetching their final state from SportMonks.Consequences:- Fixtures now get their final status written (Finished, Abandoned, etc.)
- Sparse GSI contains only 10-20 items during peak — scan is under 1 RCU, under 10ms
- Replay fixtures (negative matchIds) are cleaned up immediately instead of waiting for TTL
- New
getFixture()added toLivescoreProviderinterface for reconciliation
iOS client
ADR-010: ScrollView Paging Over TabView(.page)
ADR-010: ScrollView Paging Over TabView(.page)
| Status | Accepted |
| Date | 2026-03-24 |
TabView(.page) for horizontal date swiping. Two bugs emerged: TabView(.page) miscalculates scroll offset when adjacent pages have different content sizes (a known Apple bug), and all pages shared a single data property causing a visual flash on swipe.Decision: Replace TabView(.page) with ScrollView(.horizontal) + .scrollTargetBehavior(.paging) (iOS 17+). Using .containerRelativeFrame(.horizontal) pins each page to the container width regardless of content size, and per-page data ownership eliminates the shared-state flash.Consequences:- Fixes both the misaligned pages and visual flash bugs
- All future paged views should use this
ScrollViewpattern, neverTabView(.page) - Requires iOS 17 minimum (already the deployment target)
Display and business logic
ADR-006: Chase Win Probability Model
ADR-006: Chase Win Probability Model
| Status | Accepted |
| Date | 2026-02-27 |
rrr / crr) that ignored wickets in hand, target difficulty, and balls remaining. A team needing 12 RPO with 8 wickets was treated identically to 12 RPO with 2 wickets.Decision: Replace the naive ratio with a polynomial logistic regression (degree 2, 5 features) trained on 1,472 recent T20I matches (2023+), achieving 83.9% accuracy. The 5 features are: balls remaining (normalized), wickets in hand, RRR differential, performance index (CRR x wickets), and target (normalized). The model ships as 31 hardcoded constants — pure arithmetic, sub-microsecond evaluation, no ML runtime.Consequences:- Wickets now matter — different probabilities for same run rate with different wickets in hand
- Target-aware — chasing 220 treated differently from 130 at the same run rate
- Zero runtime cost — one dot product, one sigmoid
- Model is T20I-specific — may need retraining for franchise leagues
ADR-012: Display Mode — IPL-Only League Rendering
ADR-012: Display Mode — IPL-Only League Rendering
| Status | Accepted |
| Date | 2026-03-24 |
displayMode field controlled how match cards render team identity: league mode (brand colors, no flags) vs international mode (SportMonks flags). Two large hardcoded sets mapped every league code to a mode, but in practice only IPL needed league mode — other franchise leagues had no complaints using SportMonks flags.Decision: Simplify getDisplayMode() to: IPL -> league, everything else -> international. Remove both INTERNATIONAL_CODES and LEAGUE_CODES sets.Consequences:- Match cards for PSL, BBL, BPL etc. now render with SportMonks flags instead of brand colors
- If a league raises licensing concerns, adding its code back is a one-line change
- Trivially understandable rendering logic
Decommissioned
ADR-007: Test Strategy Decommission
ADR-007: Test Strategy Decommission
| Status | Accepted |
| Date | 2026-03-02 |
- Eliminated false sense of coverage from tests that were never gated on merge
- Strategic investment in replay-based testing that provides higher confidence for a real-time data flow system
- No unit tests exist during the transition period until the replay harness is complete