Self-hosted · Open source
Your team scores.
Your house reacts.
Goalfeed watches live NHL, MLB, CFL and NFL games for the teams you tell it to follow, and fires a Home Assistant event within a second of the score changing upstream — goal horn, lights, sirens, whatever you wire to it. It polls the same public league data your browser already uses and talks straight to your own Home Assistant instance. No account, no cloud, no subscription.
The moment
This is what a goal looks like, arriving.
The instant a watched team's score changes, Goalfeed fires a goal
event to Home Assistant, logs it, and pushes it over its WebSocket API too.
Below is a simulated feed cycling through NHL and MLB — the two leagues where a
score is always worth one point, so one score change is one event — showing
team, score, timestamp. That's genuinely all that arrives today; see the note
under the payload for why.
- 20:41:03 GOAL — TOR 2–1 WPG
- 20:36:55 PERIOD START — P2
- 20:18:29 GOAL — WPG 1–1 TOR
Simulated for this page — not a live connection.
This is what actually reaches Home Assistant when that happens — the fields every league reliably sends:
goal event
{
"id": "b3f0c9de-4b7a-4c1a-9c2e-1e5b6a2f6a10",
"teamCode": "TOR",
"teamName": "Toronto Maple Leafs",
"opponentCode": "WPG",
"opponentName": "Winnipeg Jets",
"leagueId": 1,
"leagueName": "NHL",
"gameCode": "2026020123",
"period": 2,
"gameState": {
"home": { "team": "TOR", "score": 2 },
"away": { "team": "WPG", "score": 1 },
"status": "active"
}
}
Detection is a raw score-diff, not a play-by-play feed — Goalfeed knows a
team's score went up, not who scored or how. So there's no scorer name here.
The full event also carries an internal type field, but it's
only populated for Olympic hockey today; NHL, MLB, CFL and NFL leave it
empty. Filter your automations on teamCode and
leagueId, not on type.
How it works
Four small steps, once a second.
No message queue, no database, no third-party goal API. Goalfeed is one Go binary that polls, diffs, and pushes.
Poll
Every second, Goalfeed re-checks each active game for the teams you're watching — straight from NHL, MLB, CFL and NFL's own public APIs. NFL also gets a push channel, ESPN's Fastcast WebSocket, layered on top for lower latency.
Diff
It compares the new score to the last one it saw for that game. Any increase becomes an event — there's no play-by-play involved, just the number going up.
Fire
A goal event goes to Home Assistant, a matching message
broadcasts over the WebSocket API, and an entry lands in the local JSONL log
— all three, every time, nothing routed through a third party.
React
Your Home Assistant automation runs: goal horn, lights in team colours, a script, whatever you've built.
Every upstream source Goalfeed polls — NHL, MLB, CFL, NFL, and IIHF for the
leagues that go further — is an unofficial, undocumented public API, the same
one each league's own site uses. None of them are versioned or contractually
stable, so any of them can change shape without notice. And because detection is
a raw score-diff rather than a real event feed, it counts points, not plays: a
converted touchdown in the NFL or CFL currently fires as seven separate
goal events (one per point), not one. NHL and MLB don't have
this problem — a goal is always worth one point, so one score change is one
event, which is why the demo above sticks to those two. It's honest, it's simple,
and it's the thing to know before you wire an automation that assumes more
precision than that.
Leagues
Four leagues fire goal events today.
Two more have real code behind them but aren't part of the live polling loop or a released build yet — shown here so the status is clear, not because they're ready to use.
| League | Status | What that means |
|---|---|---|
| IIHF | API only | The client and service are fully built and power the web API's schedule and history lookups, but IIHF isn't registered in the live polling loop — no Home Assistant events fire for IIHF games yet. |
| Olympic hockey | In development | Wired into live goal detection in active development, but the code isn't part of a committed, released build yet. Not something you can install today. |
Install
Three ways to run it.
Pick the one that matches how you already run Home Assistant. Full details, including every config option, are in the install docs and configuration reference.
The recommended path — no URL, no token, Home Assistant's Supervisor handles auth automatically.
- In Home Assistant: Settings → Add-ons → Add-on Store, open the ⋮ menu, choose Repositories, and add:
https://github.com/goalfeed/hassio-goalfeed-repository
- Install the Goalfeed add-on from the store.
- Open its Configuration tab and enter team codes:
nhl_teams: "TOR,WPG"
mlb_teams: "TOR,NYY"
test_goals: false
The add-on's Configuration tab currently exposes NHL and MLB only. To watch
CFL or NFL teams from the add-on, mount a config.yaml or set
GOALFEED_WATCH_* environment variables on the container — see
the Home Assistant docs.
- Start the add-on — it auto-detects Home Assistant through the Supervisor API, so there's nothing to authenticate.
- Open the web UI from the sidebar (ingress) or
http://homeassistant.local:8080.
There's no published Docker image yet — build the repo's
Dockerfile yourself.
# clone and build
git clone https://github.com/goalfeed/goalfeed.git
cd goalfeed
docker build -t goalfeed .
# run it — GOALFEED_WEB=true starts the API/UI without overriding the container's CMD
docker run -d --name goalfeed -p 8080:8080 \
-e GOALFEED_WEB=true \
-e GOALFEED_WATCH_NHL=WPG \
-e GOALFEED_HOME_ASSISTANT_URL=http://homeassistant.local:8123 \
-e GOALFEED_HOME_ASSISTANT_ACCESS_TOKEN=your-long-lived-token \
goalfeed
This Dockerfile builds the Go binary only — it doesn't bundle
the React web UI, so this image serves the REST and WebSocket API but not
the scoreboard page. For the full web UI in a container, build the frontend
in yourself, or use the release
tarball below, which ships both.
Every push to main publishes a
GitHub Release
for Linux, macOS and Windows (amd64/arm64/arm/386), each bundled with the built
web UI.
# swap linux_amd64 for darwin_arm64, windows_amd64, etc.
gh release download --repo goalfeed/goalfeed --pattern "*linux_amd64.tar.gz"
tar xzf goalfeed_*_linux_amd64.tar.gz
./goalfeed --nhl WPG --web
No gh? Grab the matching archive by hand from the
Releases page.
Home Assistant
The goal horn, wired up.
Goalfeed authenticates to Home Assistant with a long-lived access token (or,
on the add-on, the Supervisor handles it for you) and posts to
/api/events/goal. Every event Goalfeed sends — goal detections,
period-start notices, test goals — arrives under that same event type,
goal, regardless of sport. Filter on the event's
teamCode, not on its internal type field.
- 01Trigger on the
goalevent, filtered to your team's code. - 02Play the goal horn on whatever media player is in the room.
- 03Flash the lights in team colour, then bring them back.
Full walkthrough, including per-team sensors and binary sensors Goalfeed also publishes, is in the Home Assistant guide.
# Winnipeg Jets goal horn
automation:
- alias: "Goalfeed - Jets score"
trigger:
- platform: event
event_type: goal
event_data:
teamCode: WPG
action:
- service: media_player.play_media
target:
entity_id: media_player.living_room_speaker
data:
media_content_id: /local/sounds/goal_horn.mp3
media_content_type: music
- service: light.turn_on
target:
entity_id: light.living_room
data:
color_name: blue
flash: long
- delay: "00:00:03"
- service: light.turn_on
target:
entity_id: light.living_room
data:
color_name: white
For developers
Home Assistant isn't the only consumer.
Start Goalfeed with --web and you get a plain REST API, a
WebSocket feed, and the bundled scoreboard UI, all on one port. Build a
physical goal-light rig, a Discord bot, a browser overlay — anything that can
make an HTTP request or open a socket.
/api/games, /api/events, /api/teams…
Active games, historical/upcoming games by date, recent events, team lists,
and Home Assistant status/config — all under ApiResponse
{success, data, message}. Full reference: API
docs, or Swagger UI at /swagger/index.html on your own
instance.
ws://your-host:8080/ws
Connects immediately to a games_list snapshot, then streams
game_update, event, and log messages
as they happen. No auth, no ping/pong required. Full message shapes:
WebSocket docs.
No auth, open CORS, by design
Both APIs accept unauthenticated requests from any origin — fine behind Home Assistant ingress or on a home network, not something to expose directly to the internet. See troubleshooting if something isn't reaching you.