WebSocket reference
The real-time push feed behind the bundled web UI: connection
lifecycle, message envelope, and what actually arrives in each message —
including where it differs from the repo's own docs/WEBSOCKET.md.
Minimal working example
websocat ws://localhost:8080/ws
Connecting alone produces one message immediately — a full snapshot of every active game, before you do anything else:
{ "type": "games_list", "data": [] }
(An empty array is the correct, expected first message if nothing is active yet — not an error.)
Connection lifecycle
- Endpoint:
ws://<host>:<port>/ws(wss://if you terminate TLS in front of it — Goalfeed itself doesn't). - No authentication, all origins allowed
(
CheckOriginalways returnstrue) — same posture as the REST API, see Security. - On connect: the server immediately sends a
games_listsnapshot of every game currently in the in-memory store. - Thereafter: the server pushes
game_update,event,games_list, andlogmessages as they happen. There is no polling needed on the client side. - No ping/pong required. The server does read incoming frames in a loop, but only to detect disconnect — nothing in the server processes message content sent by the client. This is a receive-only protocol in practice: server → client.
- No reconnection support server-side. If the connection drops,
the client must reconnect and re-request state itself (a fresh connection gets a
new
games_listsnapshot automatically, so a naive "reconnect and listen again" strategy is enough — there's no sequence number or replay to manage). - Broadcasts are non-blocking. Every broadcast uses
select { case hub.broadcast <- data: default: }— if the internal buffer (1024 messages) is full, the message is dropped rather than blocking the sender. A slow or stalled client can silently miss messages instead of slowing down everyone else; there's no back-pressure or delivery guarantee.
Message envelope
{ "type": "games_list" | "game_update" | "event" | "log", "data": { ... } }
| Type | data shape | Sent when |
|---|---|---|
games_list | array of models.Game |
on connect, and whenever the active-games set changes |
game_update | single models.Game |
a game's score/period/status changed |
event | single models.Event |
a goal/score event fired — see the sharp edge below |
log | single models.AppLogEntry |
an entry was appended to the app log (event delivery result, or a state change) |
The repository's own docs/WEBSOCKET.md shows this example
event payload:
{
"type": "event",
"data": {
"type": "goal",
"description": "Goal scored by Player Name",
"teamCode": "TOR",
...
}
}
As read from the current league services, NHL, MLB, NFL, and CFL never
set Event.Type or Event.Description —
those fields are left at their Go zero values, "". A real
event message from any of the four leagues that actually fire live
goals looks like this instead:
{
"type": "event",
"data": {
"id": "...",
"type": "",
"description": "",
"teamCode": "WPG",
"teamName": "Winnipeg Jets",
"leagueId": 1,
"leagueName": "NHL",
"period": 2,
"score": { "homeScore": 2, "awayScore": 1, "homeTeam": "WPG", "awayTeam": "TOR" }
}
}
The only league service that currently populates type and
description is Olympic hockey — which is itself
untracked/work-in-progress (see the leagues
reference). If you're building a consumer, filter on teamCode and
leagueId, not on data.type — exactly the same
guidance as the Home Assistant event, since
both come from the same underlying models.Event.
Field reference: event data
| Field | Type | Populated for NHL/MLB/NFL/CFL? |
|---|---|---|
id | string | yes |
type | string (EventType enum) |
no — always "" |
description | string | no — always "" |
teamCode / teamName | string | yes |
opponentCode / opponentName | string | yes |
leagueId / leagueName | int / string | yes |
period | int | yes |
score | {homeScore, awayScore, homeTeam, awayTeam} | yes |
details | nested, sport-specific (down,
inning, goalType, ...) |
partially — inconsistent across leagues, treat as best-effort |
Next
Same event data reaching Home Assistant instead — Home Assistant automations. Prefer polling? The same data is available over REST.