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.
What is the replay system
SportMonks has no sandbox mode. When no matches are live,/livescores returns {"data": []}. The replay system solves this by letting you exercise the full data flow — poll, parse, write, push — using pre-recorded match data whenever you need it.
A replay runs through the exact same pipeline as a real match. The backend polls for scores, writes to DynamoDB, runs the MatchEventBus detection pipeline, and sends APNs pushes. The only difference is the data source: a standalone mock service instead of the real SportMonks API.
Architecture
Three services participate in a replay:| Service | Role | Deployment |
|---|---|---|
| Backend | Creates tapes from SportMonks data, runs the ReplayScore poller | App Runner |
| mock-sportmonks | Standalone Express service that stores tapes in S3 and serves them as a SportMonks-compatible API | App Runner (separate service) |
| Pavilion (Admin iOS) | UI for picking a fixture and triggering tape creation | iOS app |
SportMonksClient interface — a MockSportMonksClient that points at a different base URL. Zero code changes on the consumer side.
Tape lifecycle
A tape is a snapshot of a completed match — all balls, batting, bowling, and runs — stored in S3 and served back ball-by-ball on a timer.S3 tape structure
Each tape is two files:Clock-based cursor
The replay does not use a timer that increments a cursor on each tick. Instead, it derives the current position from wall clock time:Innings break simulation
When the raw cursor reaches the end of the first innings,computeEffectiveCursor() freezes the effective cursor at the last S1 ball. The freeze lasts for ceil(20 minutes / msPerBall) virtual ticks. During the break, /livescores returns status: "Innings Break". After the break window passes, the effective cursor resumes from the first S2 ball.
Post-match window
SportMonks keeps finished fixtures in/livescores for about 15 minutes after completion. The replay mirrors this — after all balls are played, the fixture stays visible in /livescores for 15 minutes, then is evicted.
Replay data flow
Once a tape is active, the ReplayScore poller picks it up on its next tick.Two pollers, one codebase
Both the LiveScore and ReplayScore pollers are instances of the sameScorePoller class. The only differences are configuration:
| Config | LiveScore | ReplayScore |
|---|---|---|
name | LiveScore | ReplayScore |
fetchFn | sportMonksClient.getLivescores() | mockClient.getLivescores() |
lockId | live-score-poller | replay-score-poller |
resilience | true (cockatiel circuit breaker) | false (bare fetch) |
replay | false | true |
intervalMs | 5000 | 5000 |
Complete isolation
| Scenario | LiveScore poller | ReplayScore poller |
|---|---|---|
| SportMonks down | Circuit opens, alerts fire | Unaffected |
| mock-sportmonks down | Unaffected | Logs error, no alerts |
| DynamoDB down | Both fail independently | Both fail independently |
Negative matchId convention
Replay matches use negative IDs (e.g.,-69475). This is the core mechanism for distinguishing replay from real data.
When ScorePoller runs with replay: true, it negates the fixture ID before writing:
createReplayFixture method in FixtureService also writes with the negative ID:
Where negative IDs appear
| Location | How it’s used |
|---|---|
FixtureService.createReplayFixture() | Writes matchId: -fixtureId to DynamoDB |
ScorePoller.tick() | Negates fixture ID when replay: true |
FixtureRepository.scanLiveIndex() | NOT begins_with(matchId, :dash) filter |
user.ts route handler | matchId.startsWith('-') guard |
matches.ts route handler | Passes includeReplays: true for admin users |
Admin controls
The admin sendsPATCH /admin/matches/:matchId to control a replay. Internally, this updates mock-sportmonks state (speed changes rebase startedAt to preserve the current cursor position at the new speed).
| Action | Effect |
|---|---|
play | Start or resume replay (accepts optional speed multiplier) |
pause | Pause replay at current position |
seek | Jump to a specific ball position |
setSpeed | Change playback speed (e.g., 2x, 5x) |
reset | Reset replay to the beginning |
msPerBall: 3000, a full T20 (~480 balls across both innings) replays in roughly 24 minutes plus a simulated 20-minute innings break.
Speed changes use a rebase approach: the system computes the current raw cursor at the old speed, then sets a new
startedAt such that the same cursor position is maintained at the new speed. No ball is skipped or repeated.Visibility
Replay matches are invisible to regular users by default. DynamoDB filtering: TheGET /matches query applies a FilterExpression NOT begins_with(matchId, :dash) to exclude negative-ID fixtures. No GSI required.
Admin visibility: The matches.ts route passes includeReplays: true when the request comes from an admin user (checked via Cognito group membership). Admin users see replay fixtures alongside real matches in the iOS app with a “Match Replay” label.
Replay fixture TTL: Every replay fixture is written with a ttl field set to now + 12 hours. While the replay is active, the poller extends the TTL on each tick. After the replay stops, the TTL expires and DynamoDB auto-deletes the item. No cleanup job needed.
Startup gating
The ReplayScore poller only starts whenMOCK_SPORTMONKS_URL is set. If the env var is absent, no MockSportMonksClient is created and the poller never starts.
| Environment | LiveScore poller | ReplayScore poller |
|---|---|---|
| Production | Enabled | Enabled (both run side by side) |
| Sandbox | Disabled (DISABLE_SPORTMONKS_POLLING=true) | Enabled |
| Local dev | Disabled | Disabled (no MOCK_SPORTMONKS_URL) |
Gotchas
15-minute start delay
15-minute start delay
When a tape is created, both the backend fixture and the mock-sportmonks state use a
startedAt 15 minutes in the future. This simulates the “Not Started” window that real matches have. The backend creates the fixture with status: NS and startTime 15 minutes out. mock-sportmonks computes rawCursor = floor((now - startedAt) / msPerBall) — so no balls are served until startedAt arrives.Replay fixtures use current date, not original match date
Replay fixtures use current date, not original match date
The
createReplayFixture method sets startTime and date to the current timestamp (plus 15 minutes), not the original match date from SportMonks. This is intentional — it lets the fixture appear in “today’s matches” queries — but means the date shown in the app will not match the original match.mock-sportmonks does respect include filtering
mock-sportmonks does respect include filtering
Unlike the old in-process replay system, mock-sportmonks correctly handles the
include query parameter. It parses SportMonks-style nested includes (e.g., balls.score,balls.bowler) and only returns requested sub-resources. A bare /livescores call returns the fixture shell without runs, batting, bowling, or balls.Batting and bowling are filtered to balls played
Batting and bowling are filtered to balls played
The fixture builder filters batting and bowling arrays to only include players who have appeared in balls up to the current cursor. A batsman who enters at ball 100 will not appear when the replay is at ball 45. This prevents leaking future player data.
mock-sportmonks resumes after restarts
mock-sportmonks resumes after restarts
On startup, mock-sportmonks reads all
state.json files from S3 with status: playing and resumes them. The clock-based cursor catches up to where the match should be. If the match finished during downtime, it is marked as finished immediately.Idempotent tape creation
Idempotent tape creation
Calling
POST /admin/tapes/:fixtureId twice does not create a duplicate. The first call stores the tape and starts the replay. Subsequent calls update msPerBall (rebasing startedAt to preserve cursor position) and return {created: false}.Key files
| File | Purpose |
|---|---|
backend/src/services/tapeService.ts | Fetches match from SportMonks, posts tape to mock-sportmonks, creates replay fixture |
backend/src/clients/MockSportMonksClient.ts | HTTP client for mock-sportmonks (same interface as SportMonksClient) |
backend/src/jobs/replayScoreJob.ts | Configures and starts the ReplayScore poller |
backend/src/jobs/ScorePoller.ts | Shared poller class used by both LiveScore and ReplayScore |
backend/src/services/FixtureService.ts | createReplayFixture() — writes negative-ID fixture to DynamoDB |
mock-sportmonks/src/services/replayManager.ts | Core replay logic — clock-based cursor, S3 state management, post-match window |
mock-sportmonks/src/services/fixtureBuilder.ts | Reconstructs SportMonks-shaped fixture from tape data at current cursor |
mock-sportmonks/src/repositories/TapeRepository.ts | S3 read/write for tape.json and state.json |