[customer-story]

Real-time tournaments without the polling tax

How Bet Max Tourney delivers sub-second bracket updates, race-fair invites, and in-app notifications for a tournament platform that lives and dies by latency.

6 min read
Tournament lobby: open entries and live invites stream in via SSE, no manual refresh.
Tournament lobby: open entries and live invites stream in via SSE, no manual refresh.
Live play: brackets, odds, and balances stay current as the game moves.
Live play: brackets, odds, and balances stay current as the game moves.

“SSE replaced three polling routes and an event bus with one socket. The brackets update as fast as the games change.”

— David Reiter, Founder, Bet Max Tourney

For a tournament platform, latency is the product. Bet Max Tourney runs daily sports betting tournaments where invite races, live brackets, and instant scoring decide whether players stay engaged. Five seconds of stale state is a lost player.

The challenge

A tournament platform that polls is a tournament platform with three problems.

Invite races. Two players hit “join” on the same open slot. One gets in. The other sees stale state and a confusing error. The retry costs the platform a player.

Live brackets. Friends watching the same game on different phones see scores drift by five to fifteen seconds. The competitive feel collapses.

Tournament start. A user opens the app late because the “starting soon” alert never arrived. The first round is already locked.

The polling alternative trades one set of problems for another: stale data, doubled server load, drained mobile batteries, and the same fundamental issue. The client is asking when it should be told.

The solution

Stream Sync Engage gave Bet Max Tourney a single durable event spine: one HTTP streaming connection per player, server-pushed updates the moment state changes, and per-user targeting that keeps private events private.

Three design choices make this work in production.

The database is the event. Postgres triggers on coreTournaments and coreTournamentInvites write directly into the SSE table. There is no application-level fanout to remember, no second source of truth to drift out of sync. When a tournament’s state changes, the trigger emits. Always.

Privacy by targeting, not filtering. Invite events carry a criteria.sseid field. SSE filters at query time so player A never receives player B’s invite. The client doesn’t filter, doesn’t compare, doesn’t have to be trusted.

Reconnect-aware sessions. The client carries a session ID across tab refreshes and brief disconnects. SSE’s 24-minute message TTL means a player who reconnects sees everything they missed during the gap.

In the stack

The integration splits cleanly between Postgres and the Vue 3 PWA.

On the database side, a trigger emits a tournament sync event whenever the tournament table changes. The trigger does one thing: write the current set of active tournaments to SSE.

INSERT INTO appSSE (app, msg, guid, class, window_end_time)
VALUES (
  '{"app":"BMACORE"}'::jsonb,
  tournaments_json,
  'sse_tournaments_' || EXTRACT(EPOCH FROM NOW())::text,
  '[{"class":"TOURNAMENT_SYNC"}]'::jsonb,
  NOW() + '24 minutes'::interval
);

Invite events go one step further with per-user targeting. The same insert pattern, plus a criteria field that scopes the event to a single recipient.

INSERT INTO appSSE (app, criteria, msg, guid, class, window_end_time)
VALUES (
  '{"app":"BMACORE"}'::jsonb,
  jsonb_build_object('sseid', invitee_sseid),
  payload,
  guid,
  '[{"class":"INVITE_SYNC"}]'::jsonb,
  NOW() + '24 minutes'::interval
);

On the client, the Vue 3 PWA opens a single EventSource and routes incoming messages through a PubSub broker so the transport stays decoupled from the handlers.

const sse = new EventSource(`/v5/sse?sseid=${userId}&session=${sessionId}`);

sse.onmessage = (event) => {
  const { class: type, msg } = JSON.parse(event.data);

  if (type === 'TOURNAMENT_SYNC') hydrateTournaments(JSON.parse(msg));
  if (type === 'INVITE_SYNC')     updateInvite(JSON.parse(msg)[0]);
  if (type === 'MVV_TOAST')       showToast(msg, { sound: true, haptic: true });
};

That last branch is where the platform stops feeling like a web app. Important events fire haptic vibration and an audio cue at the same instant the toast appears. Tournament starting, invite accepted, you’re up in the bracket. The phone buzzes. The system feels native.

What players notice

When SSE is doing its job correctly, players do not notice SSE at all. They notice the things that go away.

The bracket refresh button is gone. The pull-to-refresh gesture is gone. The five-second “did my friend accept yet” doubt is gone. The race-condition error on a full pool is gone. The “is the app broken or did the tournament not start” moment is gone.

What remains is a tournament that updates as fast as the game updates, on every device the player owns, at the same time.

What the engineering team notices

One primitive instead of three. The team that used to maintain a polling cache layer, an in-house pub/sub bus, and a notification fan-out service now has a single event channel. New features ship against INSERT INTO appSSE. The client handler is twenty lines. The reconnect path is a parameter on new EventSource.

The deliverability meter built into the client (a small counter that compares server emissions against client receipts) means operational issues surface as a number on a dashboard, not a user support ticket.

Where this is going

Bet Max Tourney’s next phase extends the same event spine to native push notifications and email delivery, so a player who closes the app still gets the tournament start alert on their lock screen. Because SSE is already the source of truth, those channels are additional subscribers, not parallel systems.

[outcomes]
  • 3
    Polling routes eliminated
    verify before publish
  • <200ms
    Tournament state propagation
    verify before publish
  • Built in
    Reconnect-aware session resumption
[tags]
  • #real-time
  • #sports
  • #tournaments
  • #postgres
  • #websockets
[your-turn]

Build something similar?

Stream Sync Engage is the real-time event spine behind tournament platforms, live dashboards, and context-aware mobile apps. Talk to us about your stack.