Commit before publish
Presentation facts and immutable payloads commit before external side effects. A crash does not erase the intended transition.
Verdict: The recovery protocol is thoughtfully built, but the game protocol is not sound under its full failure model.
Safe today only if: IVS publishes before each deadline, transient timer callbacks do not fail, and device clocks are close to server time.
The core split is good: Ably carries immutable payloads and IVS carries the presentation cursor. The break is one layer earlier. A round becomes consequential before the system knows that the audience received its clock.
The implementation is not one realtime stream. It is four coordinated planes with different durability and ordering guarantees.
| State | Authority | Cursor / clock | Repair |
|---|---|---|---|
| Show lifecycle | PlanetScale | controlRevision, stateRevision | Fresh snapshot |
| Presentation payload | PlanetScale event log | Session-scoped eventId | Exact range replay |
| Visible presentation | IVS timed metadata | presentThroughEventId | Repeated watermark |
| Answer lock | Local SQLite WAL | Per-round settlement watermark | Host-local reopen |
Protocol law A payload may be known before it is visible. It becomes visible only when IVS advances the presentation cursor.
This is not a rewrite recommendation. Several difficult invariants are implemented correctly and should be preserved.
Presentation facts and immutable payloads commit before external side effects. A crash does not erase the intended transition.
The server awaits Ably acknowledgement before IVS can expose an event ID, preventing a normal cue from outrunning its payload.
Expo buffers synchronized events and applies a contiguous range atomically only through the IVS watermark.
Missing presentation IDs are fetched as a complete bounded range and validated for identity, order, and integrity.
A round/user unique lock and stable submission key make HTTP retries safe after lost acknowledgements.
Active permits and the local settlement watermark prevent in-process batches from being tallied halfway through a commit.
Ordered by player and show impact. Each finding is a concrete execution trace, not a theoretical consistency preference.
The server starts openedAt and closesAt, commits question_live, returns success, and schedules settlement before the background IVS publish succeeds. Retries are suppressed once the answer window expires.
live-gameplay-ledger.ts:243-248 gameplay.ts:951-1016 question-open-visibility.ts:44-51
Expo stores the selected answer before HTTP succeeds. Network and 5xx exhaustion retain it. Results treat that local selection as the player's outcome when no server lock exists, and terminal projection promotes a still-active local player to winner.
GameCoordinator.tsx:2158-2249 game-reducer.ts:814-830 game-reducer.ts:1020-1062
After bounded retries fail, request keys remain claimed while pending work is cleared. Later IVS heartbeats cannot request the same personalization again.
Immediate IDs come from a process-local map and Date.now(). The snapshot boundary filters only events already buffered during reconcile(); an older event delivered afterward is accepted without another boundary check.
game.status can overwrite a newer snapshot. After restart or clock skew, that stale high ID can also make Expo reject valid replacement-process events as out of order.ably-realtime.ts:17-19 ably-realtime.ts:333-342 reconciliation.ts:13-48
Answer locks do not advance the show revision. If older personalization has the same revision, snapshot merge logic preserves it and ignores a newly returned non-null lock.
If the callback's database query or transaction fails, the catch path logs only. It does not retry and does not leave a durable transition intent.
answer.results and its counts.The first authoritative wall-clock check occurs after authentication, profile, audience, legal, token, and game access reads.
Expo maintains a monotonic question anchor, but question closure, countdown ticks, and absolute giveaway closure still compare against Date.now() without a server-time offset.
GameCoordinator.tsx:1820-1861 CountdownTimer.tsx:61-94 GameCoordinator.tsx:1973-1980
Local SQLite and presentation events retain milliseconds. Durable rounds.openedAt and rounds.closesAt use MySQL timestamps without fsp: 3.
The system can be internally consistent and still produce the wrong game result.
The local ledger records openedAt and closesAt.
PlanetScale says question_live. The immutable event is retained. HTTP returns 201.
The question payload exists in Ably, but no IVS watermark reveals it. A background retry is scheduled.
Visibility checks now suppress every question publish retry. No player has seen the card.
The protocol correctly drains zero answers and deterministically eliminates the roster.
Safety violation A consequential round was settled without durable evidence that its presentation clock was ever accepted by IVS.
The first two changes establish the missing safety boundary. The rest make recovery converge monotonically.
Add a durable publishing to visible transition. Settlement must require durable IVS publish evidence. If first delivery never succeeds, automatically void instead of eliminating players.
Represent reveal-to-results and deadline work as persisted intents with idempotent workers. Timers should wake work, not be the only record that work exists.
A non-null answer lock must dominate an older absence. Apply the snapshot boundary to every later immediate event, not just the buffer present at reconciliation time.
Capture trusted request ingress time. Use IVS-relative elapsed time or an explicit server clock offset on clients. Store all authoritative timestamps at millisecond precision.
Exercise publish outage through deadline, two owners, response loss before reconnect, post-boundary delayed events, timer callback failure, and device clock skew.
This was a static, end-to-end protocol audit of maintained server, Expo, and admin paths at commit 9c3b609. The review traced authority, publication, IVS timing, snapshot reconciliation, answer admission, settlement, process restart, and operator feedback.
The event cursor is the strongest part of this design. The next step is to give delivery and server ownership equally explicit authority.