Building Your Own¶
There are four ways to put the feed behind your own display or integration. They differ in where your credentials live, who can see the page, and how much code you write.
| Approach | You write | Credentials live | Good for |
|---|---|---|---|
| Agent + your own page | HTML, CSS and JavaScript only | In the agent, on your computer | OBS overlays and venue displays driven from one computer |
| Node.js service | A Node.js program using @scorbit/feed |
In your service's environment | Integrations, bots, data into your own systems |
| Public website | A server that holds the feed and relays updates to visitors | On your server only | Live scores on a public website or app |
| Browser page | A server that opens the feed, and a page that attaches to it | API key on your server; feed token in the page | Your own kiosk or venue display, or a staff-only page |
All four use the same feed and the same update data.
Agent + Your Own Page¶
The simplest approach, and the right one for OBS. Run the scorbit-feed agent, and write a page
that reads the feed from it on localhost. Your page never handles a credential.
The agent serves your folder at http://127.0.0.1:8787/ and the live feed alongside it:
GET /statereturns the latest state of every machine.GET /eventsstreams changes as Server-Sent Events.
A minimal page that renders each update:
<div id="scores"></div>
<script>
const events = new EventSource("/events");
events.addEventListener("state", (e) => {
const { machines } = JSON.parse(e.data);
const list = document.getElementById("scores");
list.replaceChildren(
...machines.map((m) => {
const p = document.createElement("p");
p.textContent = `${m.game_name ?? "Machine"}: ${m.scores.map((s) => s.score).join(" / ")}`;
return p;
}),
);
});
</script>
Start from the starter overlay for a complete example, and see The Agent for every route and option.
Node.js Service¶
Use the @scorbit/feed library in a Node.js 22+ program to open a feed and react to updates.
See what your key covers¶
import { listMachines } from "@scorbit/feed";
const { scope_type, machines } = await listMachines({ apiKey: process.env.SCORBIT_API_KEY! });
for (const m of machines) console.log(m.uuid, m.game_name, m.venue.name);
Open a feed and handle updates¶
import { openFeed } from "@scorbit/feed";
const { feed, created } = await openFeed({
apiKey: process.env.SCORBIT_API_KEY!,
machines: ["<venue-machine-uuid>", "<venue-machine-uuid>"], // optional: omit for the key's whole scope
transport: "sdk", // or "sse"
});
feed.on("machines", ({ added, removed }) => console.log("joined", added, "left", removed));
feed.on("update", (update) => {
for (const machine of update.payload.machines) {
console.log(machine.game_name, machine.scores.map((s) => s.score));
}
});
feed.on("status", (status) => console.log("status:", status));
feed.on("ended", ({ reason, error }) => console.log("ended:", reason, error?.code));
feed.start(); // returns at once; updates arrive through the listeners above
// On shutdown (Ctrl-C, or a service manager such as systemd, Docker or Kubernetes),
// disconnect and delete the feed so it doesn't keep holding a live-feed slot.
for (const signal of ["SIGINT", "SIGTERM"]) {
process.once(signal, async () => {
await feed.stop().catch(() => {});
process.exit(0);
});
}
created includes the feed token (feed_token). Keep it out of logs.
Public Website: Relay Through Your Server¶
For a page anyone on the internet can open, the most reliable setup keeps every credential on your server. Your server holds the feed and relays each update to visitors over its own connection, so one feed serves every visitor and nothing a visitor does can interrupt it.
import { createServer, type ServerResponse } from "node:http";
import { openFeed, type Feed } from "@scorbit/feed";
let latest: string | null = null; // the last update, sent to each new visitor at once
const visitors = new Set<ServerResponse>(); // open connections from visitors' browsers
let current: Feed | undefined; // the feed this server holds
let shuttingDown = false;
function broadcast(payload: string) {
latest = payload;
for (const res of visitors) {
// Drop a visitor that isn't keeping up; their browser reconnects and gets the latest state.
if (res.writableLength > 1_000_000) {
res.destroy();
visitors.delete(res);
continue;
}
res.write(`data: ${payload}\n\n`);
}
}
async function startFeed() {
try {
const { feed } = await openFeed({ apiKey: process.env.SCORBIT_API_KEY! });
current = feed;
feed.on("update", (update) => broadcast(JSON.stringify(update.payload)));
feed.on("ended", () => {
broadcast(JSON.stringify({ machines: [] })); // stop showing scores from an ended feed
if (!shuttingDown) setTimeout(startFeed, 30_000); // open a new feed after a pause
});
feed.start();
} catch (err) {
console.error("could not open the feed:", err);
if (!shuttingDown) setTimeout(startFeed, 30_000);
}
}
await startFeed();
// Visitors connect to /live and receive updates as Server-Sent Events.
createServer((req, res) => {
if (req.url !== "/live") return res.writeHead(404).end();
res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache" });
if (latest) res.write(`data: ${latest}\n\n`);
visitors.add(res);
req.on("close", () => visitors.delete(res));
}).listen(3000);
for (const signal of ["SIGINT", "SIGTERM"]) {
process.once(signal, async () => {
shuttingDown = true;
await current?.stop().catch(() => {});
process.exit(0);
});
}
Serve your page from the same server, and read the relay with an EventSource:
One feed serves any number of visitors this way, so you stay well inside your two live feeds.
- When the feed ends, the relay clears what visitors see, so nobody is left looking at old scores, and opens a new feed 30 seconds later. The pause keeps a persistent problem, such as a revoked key, from hitting the rate limits.
- A visitor whose connection can't keep up is dropped; their browser reconnects by itself and gets the latest state.
Browser Page: Attach in the Browser¶
The browser can also attach to the feed directly. Your server opens the feed with the API key and
hands the page only its feed_id and feed token (sbf_...), in a response body, never in the
page's URL. See Credentials and Security.
Best for your own displays
A feed token can also end its feed. Deleting a feed loses nothing: your server can simply open a new one. But on a page the public can open, a visitor could interrupt the live display for everyone until it does. Attaching in the browser is ideal for your own kiosk, venue display or a staff-only page; for a public website, the relay keeps the display running regardless.
On the page, attach with the feed token:
import { attachFeed } from "@scorbit/feed";
const feed = attachFeed({ feedId, feedToken }); // from your server's response body
feed.on("update", render);
feed.start();
Without a bundler, load the browser build from a CDN. It exposes attachFeed as the global
ScorbitFeed. The browser build leaves out createFeed and openFeed on purpose, because they need
the API key.
<script src="https://unpkg.com/@scorbit/feed@0.1.0/dist/scorbit-feed.iife.js"></script>
<script>
const feed = ScorbitFeed.attachFeed({ feedId, feedToken }); // from your server's response body
feed.on("update", render);
feed.start();
</script>
Library Reference¶
| Function | Returns | Options |
|---|---|---|
listMachines(options) |
Promise<MachineScope> |
{ apiKey, baseUrl?, fetch?, dangerouslyAllowBrowser? } |
createFeed(options) |
Promise<CreatedFeed> |
{ apiKey, machines?, transport?, baseUrl?, fetch?, dangerouslyAllowBrowser? } |
attachFeed(options) |
Feed |
{ feedId, feedToken, baseUrl?, transport?, initialTokens?, fetch?, websocket? } |
openFeed(options) |
Promise<{ feed, created }> |
createFeed, then attachFeed with the create response |
feed.start() |
void |
Connect and keep the feed's access renewed |
feed.stop({ deleteFeed = true }) |
Promise<void> |
Disconnect and, by default, delete the feed |
feed.on(event, listener) |
unsubscribe function | See the events below |
createFeed, openFeed and listMachines need the API key and refuse to run in a browser.
attachFeed needs a feed token and refuses anything else, including an API key.
Events¶
| Event | Payload | When |
|---|---|---|
update |
the update message | Every change to any machine in the feed |
machines |
{ added, removed, machines } (machine uuids) |
The set of machines in the feed changed, just before the update that carries it |
status |
idle, connecting, live, reconnecting or ended |
The connection state changed |
ended |
{ reason, error } |
The feed ended. See Why a feed ends |
error |
an Error |
A problem the library recovers from by itself, such as a dropped connection. Never contains a credential |
feed.machines always holds the current set of machine uuids.
Transports¶
Both transports carry the same messages; sdk is the default.
sdk: a WebSocket connection with delta compression, using the officialcentrifugeclient.sse: Server-Sent Events over plainfetch. Use it where WebSockets are blocked.
Rendering Tips¶
- Key everything by
machine_uuid, never by position. On a feed that follows a venue, machines join and leave while it runs. - Handle an empty feed. A venue feed can briefly cover no machines.
- Show final scores when
game_endedistrue: the last game finished and its scores are final. - Expect missing values.
ballcan benull, andplayerisnullfor an unclaimed slot. - Show the Scorbit attribution on every screen that shows feed data. See Branding and Media Kit.