YumKiosk YumKiosk Docs
Website Agent login Owner panel
Architecture

Real-time stack

How WebSockets and polling work together in YumKiosk.

Real-time stack

YumKiosk is a real-time product — the agent sees a new session within a second of the customer tapping Order, the kiosk sees the cart update as the agent builds it, the owner dashboard sees kiosks go online and offline live. This is powered by Laravel Reverb, a first-party WebSocket server that speaks the Pusher protocol, with a polling fallback for environments where WebSockets don't work.

The architecture

  [ Client (agent / owner / kiosk) ]
              |
     ┌────────┴────────┐
     |                 |
     ▼                 ▼
  WebSocket      HTTP long-poll
  (Reverb)        (Laravel API)
     |                 |
     └────────┬────────┘
              ▼
       [ Laravel app ]
              |
              ▼
      [ MySQL + Redis ]

Reverb is a separate process (php artisan reverb:start) running on 127.0.0.1:8080, proxied by nginx at agent.yumkiosk.com/ws/. Every real-time event is dispatched by the Laravel app via broadcast(new SomeEvent(...)), which Reverb then fans out to all subscribers of the relevant channel.

Channels

YumKiosk uses three channel types:

Private channels

Prefixed with private-. Require authentication. Used for:

  • private-agent.{id} — events for a specific agent (new incoming session, status changes).
  • private-kiosk.{id} — events for a specific kiosk (order updates, session state transitions).
  • private-session.{id} — events scoped to a single session.

Authentication happens via POST /broadcasting/auth before subscription — the client sends the channel name and their auth cookie, and the Laravel app returns a signed token the client passes to Reverb.

Presence channels

Prefixed with presence-. Also require authentication, and additionally publish a list of who's currently connected. Used for:

  • presence-owner.{id} — the owner's main dashboard, tracking which agents are online and which kiosks are connected. When the owner loads the page, they see a live roster.

Public channels

Prefixed with nothing special. Anyone can subscribe. Currently unused in YumKiosk, but reserved for future public-facing dashboards.

Events

A few key events that drive the UX:

  • IncomingSession — fired when a kiosk starts a session. Sent to private-agent.{id} for every eligible agent. The agent dashboard pops up the card.
  • SessionAccepted — fired when an agent accepts. Sent to private-kiosk.{id} so the kiosk transitions to the live-session state, and to private-agent.{id} for the other agents so their card is removed from the queue.
  • OrderUpdated — fired whenever the cart changes. Sent to private-session.{id} so both the kiosk and the agent stay in sync.
  • AgentStatusChanged — fired when an agent toggles Available/Busy/Offline. Sent to presence-owner.{id} for the owner dashboard.
  • KioskPaired, KioskOnline, KioskOffline — kiosk lifecycle events. Sent to presence-owner.{id}.

Each event has a payload, usually a Laravel Resource class that serializes the relevant model.

Polling fallback

Some environments can't use WebSockets — captive portals, corporate firewalls, old Android tablets with outdated Chrome. For these, we fall back to long-polling:

  • Kiosks poll /api/public/sessions/{id}/status every 2 seconds while waiting for a session to be accepted, and /api/public/sessions/{id}/order every 2 seconds during an active session.
  • Agent dashboards poll /api/sessions/incoming every 3 seconds when the WebSocket isn't connected.

The polling fallback is automatic — the client library detects WebSocket connection failure and starts polling without user intervention. There's a small "Connection: WebSocket" or "Connection: Polling" indicator in the corner of the debug view so developers can tell which mode they're in.

Polling is obviously worse for battery, latency, and server load, so we treat it as a last resort. Under normal conditions, 95%+ of clients run over WebSocket.

Scaling

Reverb is single-process by default, but it supports horizontal scaling via a Redis pub/sub bridge. When we outgrow a single Reverb instance, we'll add more instances behind a load balancer and flip the bridge on. The application code doesn't change — events still broadcast the same way.

For now, a single Reverb process comfortably handles several thousand concurrent connections, which is well above where we are.

Connection limits and timeouts

Reverb drops idle connections after 120 seconds of no activity. Clients send a ping every 30 seconds to keep their connection alive. If a ping fails, the client reconnects with exponential backoff up to 30 seconds.

During a reconnect, any events that were broadcast are not queued — the client misses them and has to re-sync from the REST API when it reconnects. This is fine for live dashboards but means one-shot events (like an incoming session card) can be missed during a reconnect. The polling fallback catches these cases.