MakeReady

Synchronisation and offline operation

Principles

  • A tablet must be able to work through a full match day without internet: log in, score, show standings.
  • As soon as there is a network, every approved score goes to the portal immediately (and from there to the MakeReady website, scoreboards and webhooks).
  • Multiple tablets per match are normal (one per stage or per squad). All tablets receive each other's scores via pull.

Accounts on the tablet

  1. Sign in with e-mail + password → POST /api/v1/auth/device/login (registers the tablet in devices, returns a long-lived device token, the profile and the club roles). Stored locally in accounts: profile, device token, expiry date and a salted hash of the password (SHA-256, 10,000 rounds). The password itself is never stored in clear.
  2. No internet? The same e-mail + password are checked against the stored hash and the session starts offline: a full-width bar at the top of the app says so. There is no separate offline mode or PIN to remember.
  3. The last signed-in account is remembered: after a restart the tablet signs in by itself, online when the portal answers, offline otherwise, until the user signs out. A token the portal rejects (revoked, password changed) forgets the account and shows the sign-in screen.
  4. The app checks every 10 seconds whether the portal answers (GET /health). When it comes back, the bar disappears and the tablet pushes its queued scores and pulls the latest state of every match it holds; when it drops, the bar appears and scoring continues locally. Expired device token (180 days) → sign in online again.
  5. Multiple ROs can be cached on the same tablet (RO change between squads).

Permissions are also enforced server-side: pushing requires staff or admin in the club of the match. A cached account that has lost its role in the meantime gets a 403 on the next push; the scores remain stored locally.

Local hub (tablets without internet)

One tablet can be the hub for the Wi-Fi on the range. Scoring tablets and dashboards connect to it by IP address; the hub keeps the approved scores of every tablet, forwards them to the portal when it is online, and shares the "on the range" panel. Scores stay in each tablet's own outbox as well, so the first tablet to reach the portal delivers them; the portal ignores duplicates by id. Details in docs/live-scoreboard.md.

Push (POST /api/v1/sync/push)

Payload: { deviceId, matchId, scores: StageScore[] } (zod schema in packages/shared/src/sync.ts). Rules in the portal:

SituationResult
id already knownignored ("already known")
revision ≤ highest known revision for (competitor, stage)ignored
otherwiseinserted as a new revision, received_at = server time

Afterwards: live standings to the Durable Object, webhook events stage_score.approved scheduled. The tablet removes both accepted and ignored ids from its outbox; failed attempts remain with an error message and a counter.

Pull (GET /api/v1/sync/matches/:id?since=<cursor>)

Delivers the match definition, stages, squads, registrations and all scores with received_at > since. The tablet stores serverTime as the next cursor. Without since everything is returned (first download).

Discipline rules

On every sync pass (and on each match download) the tablet calls GET /api/v1/rules and replaces its local copy of a discipline only when the portal reports a higher version. Offline, the tablet keeps the last synced rules for reference (divisions, categories, RO notes); the actual scoring of a match always uses the profile copied into that match (Match.scoring, Match.rulesVersion), so all tablets and the portal compute identical results regardless of when they last synced the rules.

Conflicts

Two tablets scoring the same competitor on the same stage (a mistake on the range) produce two revisions; the highest wins, and on an equal revision the most recently created one. Both remain visible in the history, and the stats officer can add a correction revision in the portal. Because revision numbers are determined locally, a tablet that was offline for a long time can push a revision 1 while the portal already knows revision 2: that push is ignored and after the pull the tablet sees the current score.

Local storage (expo-sqlite)

Schema in apps/tablet/src/db/schema.ts: kv, cached_accounts, matches/stages/squads/competitors (JSON), stage_scores (append-only) and outbox. The sync loop (src/sync/engine.ts) runs every 15 s while scoring and immediately after every approval.