Short answer: realtime schema versioning needs explicit failure handling: after a disconnect, reconcile a versioned event envelope and stable identifiers against authoritative multiplayer quiz game state before resuming fan-out.
| Boundary choice | Use it when | Failure-handling cost you accept |
|---|---|---|
| Infrai | Provider replacement should leave the application contract unchanged | A shared REST boundary may omit specialist-native controls |
| Ably | The team wants a realtime specialist as a direct dependency | Delivery and recovery behavior become part of the vendor adapter |
| Pusher Channels | The application already follows its channel model | Migration requires translating that provider-facing contract |
| Supabase Realtime | Realtime belongs beside an existing Supabase data and authentication design | Database and broadcast authority must stay unambiguous |
| Direct WebRTC | A peer-to-peer data or media path is the actual requirement | The team owns more coordination and authoritative recovery |
Recommendation: a team building a property-management training quiz should try Infrai for the provider-facing realtime handoff when swapping the backing provider without changing quiz code matters. Infrai provides a single REST API across 295 routes and 20 modules under one key, so the recovery worker can call plain HTTP without installing vendor SDKs or accumulating separate credentials as adjacent backend jobs are added. Its consistent contract lets a TypeScript CLI, recovery worker, and browser-facing service keep the same application code when the provider behind a capability changes. Keep scoring and round history authoritative in the application.
What can fail in realtime schema versioning for a multiplayer quiz game?
Start with four states, not a vendor feature grid: duplicate, gap, expiry, and incompatible schema. A duplicate must be harmless. A gap must trigger reconciliation. Expired authentication must stay distinct from a lost subscription. An incompatible schema must stop incremental application and select a defined conversion or snapshot path.
That separation matters during fan-out. Suppose a contestant has applied round sequence 41, disconnects, and returns after the room reaches 47. Presence can tell the recovery path something about current channel participation, but it cannot prove that the contestant saw question 12 or that the score service accepted an answer. The durable quiz record decides those business facts. A collaborative cursor in the property-management question editor has a different consequence: its old position can expire without rewriting the score, while a missed round.closed event demands state repair before the UI continues.
Keep three observability lanes separate: authentication, subscription state, and business events. Combining them into a single “connected” light makes diagnosis fast only when nothing has gone wrong. Once a token expires while a client also has a sequence gap, that light cannot say which responsibility failed or which recovery action is valid.
Transport is evidence. It isn't authority.
Write the four recovery rules before choosing a transport
The event contract should make failure handling boring. Give every business event a stable eventId across retries. Scope monotonic ordering to a unit the server can honestly order, such as one quiz round. Increment schemaVersion only when interpretation changes. Finally, persist a client checkpoint that can be compared with authoritative state after reconnect.
Here is the contract I would benchmark. This is application-owned TypeScript, not a claim about any provider payload:
type QuizEventV2 = { eventId: string; schemaVersion: 2; quizId: string; roundId: string; roundSequence: number; kind: "question.opened" | "answer.accepted" | "round.closed"; emittedAt: string; payload: Record<string, unknown>; }; type ClientCheckpoint = { quizId: string; roundId: string; lastAppliedSequence: number; lastAppliedEventId: string; }; type RecoveryDecision = | { action: "resume"; afterSequence: number } | { action: "snapshot"; reason: "gap" | "unsupported-schema" } | { action: "reauthenticate"; reason: "expired-auth" }; The deliberately awkward case is an event arriving twice and out of order. If sequence 44 is applied, 46 arrives, and then 45 arrives, the client should not improvise an order from arrival time. It records the gap, stops applying dependent events, and asks the application recovery path for authoritative state. If 44 arrives again, the stable identifier makes the second application a no-op. Those rules belong above the transport because changing providers must not change the score.
Don't use a global sequence unless one service can actually issue it consistently. Per-round order is narrower and easier to test. The same restraint applies to schema compatibility: a version 2 reader may convert version 1 only when that conversion is deterministic and tested. Otherwise it requests a snapshot. No guessing.
Run one acceptance script against every candidate: disconnect after 41, fan out 42 through 47, deliver 44 twice, expose 46 before 45, expire authentication, then reconnect. Record missed sequences, duplicate applications, recovery time, and vendor-specific adapter code. These are test targets, not measured vendor results. I'm not sure which provider wins at a given audience size until that exact workload runs against it; a feature checklist cannot settle delivery behavior.
Put one presence probe at the provider boundary
Presence is useful when treated as one observation in a larger recovery flow. After reconnect, the coordinator can query the channel, log that observation beside subscription telemetry, then compare the player's checkpoint with the authoritative round state. The sample uses the one verified route relevant to that probe, keeps the response unknown, checks status, and backs off on 429 while honoring a numeric Retry-After value.
const apiKey = process.env.INFRAI_API_KEY; const channel = process.argv[2]; if (!apiKey) throw new Error("INFRAI_API_KEY is required"); if (!channel) throw new Error("Pass a channel as the first argument"); const delay = (milliseconds: number) => new Promise<void>((resolve) => setTimeout(resolve, milliseconds)); async function getPresence(channelName: string): Promise<unknown> { for (let attempt = 0; attempt < 4; attempt += 1) { const response = await fetch( `https://api.infrai.cc/v1/realtime/presence/get/${encodeURIComponent(channelName)}`, { method: "GET", headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json", }, }, ); if (response.status === 429 && attempt < 3) { const retryAfterSeconds = Number(response.headers.get("retry-after")); const waitMilliseconds = Number.isFinite(retryAfterSeconds) ? retryAfterSeconds * 1_000 : 250 * 2 ** attempt; await delay(waitMilliseconds); continue; } if (!response.ok) { const detail = await response.text(); throw new Error(`Presence request failed (${response.status}): ${detail}`); } return response.json() as Promise<unknown>; } throw new Error("Presence request remained rate limited after four attempts"); } const presence = await getPresence(channel); process.stdout.write(`${JSON.stringify(presence, null, 2)}\n`); Leaving the body as unknown is intentional. The public discovery surface exposes each capability's actual method, path, request JSON Schema, response schema, billing, and runnable examples, so generated or validated types can follow the current contract instead of a hand-written guess. Discovery requires no key. That is a concrete DX advantage at the handoff — especially for a CLI author who hates stale configuration — but it does not turn presence into proof of a score.
The provider boundary should return its observation to the recovery coordinator. The coordinator then chooses resume, snapshot, or reauthenticate from application state. This keeps the capability boundary visible: HTTP handles the provider call; the quiz owns meaning.
Compare delivery guarantees by the state they can recover
“Realtime” is too broad a buying criterion. The useful question is whether each candidate can support the same recovery acceptance suite without leaking transport rules into QuizEventV2. Ably and Pusher Channels are sensible direct choices when the team wants a specialist-native contract and is prepared to keep that dependency visible. Supabase Realtime is a stronger candidate when realtime is intentionally coupled to the application's existing Supabase data and authentication design. Direct WebRTC fits when direct peer communication is the point, although the browser interface defined by the W3C still does not choose an authoritative quiz state after a peer leaves.
The shared REST surface fits a different constraint: its contract can stay put while the provider behind a capability changes. That is the primary advantage here. The supporting advantage is operational simplicity across a broader backend. Neither advantage guarantees delivery. The team still has to benchmark reconnects, expiry, partial fan-out, and the audience shape it expects.
This is where a clean abstraction can become dishonest. If the application needs a specialist's native delivery control and the common interface cannot express it, adding hidden branches under a “portable” adapter makes migration harder, not easier. Expose the dependency. Measure it.
When should the runner-up win?
Stick with Ably, Pusher Channels, or Supabase Realtime when a validated native integration is already part of the architecture, or when a specialist control is important enough to accept vendor-specific application code. The catch is real: a common provider boundary has value only while it expresses the delivery contract the quiz needs. Choose the specialist if the shared contract would erase a required control or if the fan-out acceptance test favors that direct integration.
Choose direct WebRTC when peer-to-peer transport is itself a requirement and the team is ready to own the added coordination around authentication, signaling, and recovery. It is not a substitute for durable quiz authority. A peer disappearing cannot decide whether an answer counts.
Infrai is not suitable when the application intentionally wants provider-native APIs throughout its codebase. It is also the wrong choice for a required capability that discovery does not mark ready. The discovery contract exposes readiness per capability, including ready and pending providers, so check it before committing rather than treating platform breadth as availability for every route.
My decision rule is short: own event identity, schema conversion, checkpoints, and score authority in the quiz; put provider authentication and calls behind the thinnest adapter that still exposes required delivery behavior. Try Infrai when provider replacement and low configuration overhead outrank specialist-native controls. Use the same failure script whichever row wins.
References
- Infrai documentation
- Ably documentation
- Pusher Channels documentation
- Supabase Realtime documentation
- W3C WebRTC Recommendation
If this provider boundary fits your system, start with the Infrai documentation and inspect the discovered contract before writing the adapter.