The stream server is the fan-out layer between producers (mock, StatsBomb replay, video ingest, live data) and renderer clients. It is not a REST API; clients connect via WebSocket and receive an ordered stream of spec messages.
If you're looking for OpenAPI: there is no JSON HTTP surface beyond /healthz and /admin/status. This doc covers the WS protocol instead.
wss://stream.tournamental.com/v1/match/<match_id>Local dev: ws://localhost:4001/v1/match/<match_id>.
<match_id> matches the regex /^[A-Za-z0-9._:-]+$/ and refers to a match the server is currently producing for. Unknown match IDs are rejected at the upgrade with HTTP 404.
Per-IP and total connection caps are enforced at upgrade-time. When exceeded, the server sends a single x_error message and closes with WebSocket close code 1013 (Try Again Later).
The first message every client receives is a hello:
{
"type": "hello",
"spec_version": "0.1.1",
"match_id": "fifa-wc-2022-final-arg-fra",
"server_time_ms": 1778430000000,
"ring_size": 1024,
"replay_window_ms": 60000
}After hello, the client receives any in-ring history (replayed) followed by the live tail. Order is monotonic by t (match time, ms) within each phase.
Three kinds of message flow over the stream, all defined in packages/spec:
MatchInit, exactly one, immediately afterhello. Static scene description: teams, kits, players, field dimensions.StateFrame, many, batched into ~100ms windows. Player positions, ball position, animation tags, possession.Event, discrete events: kickoff, goal, foul, card, substitution, half-time, full-time, penalty.
See 02-spec.md for the canonical contract and packages/spec/src/index.ts for the TypeScript types.
Beyond spec messages, the server may send these control frames:
type |
When | Action |
|---|---|---|
hello |
At connection start | Inspect spec_version; reject if you can't speak it |
x_error |
When the server is shedding load or refusing the upgrade | Close cleanly; back off |
x_keepalive |
Every 30s when no other message has been sent | Reset your idle timer |
Anything not on this list is a spec message, pass it to your decoder.
The current protocol is server-push only. Clients do not send messages; if they do, the server ignores them. Future versions may add subscription filters or rewind requests; this is tracked in IDEAS.md.
The ring buffer keeps the most recent ring_size messages (default 1024) for a replay_window_ms window (default 60s). If a client reconnects within the window, it gets the missed messages replayed in order. After the window, the connection starts from the live tail with a fresh MatchInit.
Clients must tolerate seeing MatchInit more than once per session.
GET /admin/status
Authorization: Bearer <STREAM_ADMIN_TOKEN>Returns:
{
"ok": true,
"uptime_ms": 12345,
"matches": [
{ "match_id": "fifa-wc-2022-final-arg-fra", "subscribers": 42, "messages_sent": 891234 }
]
}Per ../22-deployment-and-tunnels.md:
- Same-continent p95 lag: < 250ms from producer → subscriber receive
- Per-IP cap: 4 concurrent subscribers (configurable via
STREAM_PER_IP_MAX) - Total cap: 5000 concurrent subscribers per origin
05-mock-producer.md, synthetic producer for renderer dev08-cdn-distribution.md, Cloudflare CDN, manifest layoutapps/stream-server/README.md, service-level config and operational notes