The Godot and Unity SDKs connect with a signed player token that your own server mints. This page shows how to mint one with the stream's server key and how to connect from plain JavaScript (a web game or dashboard) over a raw WebSocket — closing the production-auth loop the SDK pages defer to "your server". For throwaway prototyping you can skip tokens entirely with Open identity (see Streams getting started).
1. Mint a token on your server
Call POST /stream/v1/tokens from your backend with the stream's server key as a Bearer credential. You assert who the player is and (optionally) which rooms they may join; FlowFn returns a short-lived signed token plus its expires_at. Keep the server key on the server — only the minted token ever reaches the browser.
// Node.js (your game server) — mint a token for one player
const res = await fetch('https://engine.flowfn.com/stream/v1/tokens', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + process.env.FLOWFN_STREAM_SERVER_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
stream: 'strm-7f3a2c1d', // your stream's public code
user_id: 'player-42', // your id for this player (required)
name: 'Ada', // optional display name shown in the room
rooms: ['match_8f3a'], // optional: restrict which rooms the token may join
ttl_seconds: 3600, // optional: defaults to ~1h, capped server-side
}),
});
const { token, expires_at } = await res.json();
// hand "token" to that player's browser (e.g. embed it in your page response)
2. Connect from the browser
Open a WebSocket to the gateway with ?stream= and ?token=, then send JSON frames: join a room, then publish to a channel. You receive every message on the rooms and channels you joined.
const ws = new WebSocket(
'wss://engine.flowfn.com/stream/v1?stream=strm-7f3a2c1d&token=' + token
);
ws.onopen = () => {
ws.send(JSON.stringify({ op: 'join', room: 'match_8f3a', ref: '1' }));
ws.send(JSON.stringify({
op: 'publish', room: 'match_8f3a', channel: 'chat',
payload: { text: 'gg!' },
}));
};
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
// msg = { room, channel, from: { user_id, name, role }, payload, ts }
console.log(msg.channel, msg.from.name, msg.payload);
};
Prototyping without a backend? Turn on Open identity and connect with ?stream=strm-7f3a2c1d&user_id=player-42&name=Ada instead of a token — but anyone with the URL can then claim any id, so switch to minted tokens before launch.
The message your automations receive
When a channel has a binding, every published message also delivers this exact payload to your workflow, stream function, or agent:
{
"stream": "strm-7f3a2c1d",
"room": "match_8f3a",
"channel": "chat",
"from": { "user_id": "player-42", "name": "Ada", "role": "player" },
"payload": { "text": "gg!" },
"ts": 1718000000000
}
A bound stream function (the stream's Code tab) reads that and answers the room with ctx.streams.publish:
// Stream Code tab — bind the "chat" channel to this function
exports.onChat = async (msg, ctx) => {
// msg = { stream, room, channel, from, payload, ts }
if (msg.from.role !== 'player') return; // ignore server/flowfn echoes
const text = String(msg.payload && msg.payload.text || '');
const reply = text.toLowerCase().includes('help')
? 'Type /spawn to add a bot.'
: 'Heard you, ' + (msg.from.name || 'player') + '!';
// publish onto a flowfn-authority channel players can hear but never write
await ctx.streams.publish(msg.stream, msg.room, 'ai', { text: reply });
};
FlowFn-side publishes never re-trigger bindings, so replying into your own channel cannot loop. Anything the function logs with console.log — or any error it throws — shows in the stream's Recent activity feed, which is where to look when a binding "didn't work".