Realtime multiplayer
DavidnetSDK.realtime is a generic, content-agnostic realtime layer under its own namespace: named rooms (pub/sub channels
with presence and a shared state channel), a matchmaking queue, and a lobby-wide announcement channel.
The platform never looks at what you send, so the exact same API works for a 2-player turn-based
game, a 50+ player action game, or a one-way live feed (e.g. a price ticker) with no "players" at
all. There is no maximum room size, queue size, or group size. Every method auto-connects on first
use — you don't need to call connect() yourself unless you want to open the connection early.
Rooms
DavidnetSDK.realtime.joinRoom(room)— joins a named room (created on first join, destroyed when empty; join as many as you like). Resolves:{ room, members, state }—membersis everyone already in the room, each{ userId, username, displayName, avatarUrl },stateis a snapshot of every key/value set in this room so far viasetState.DavidnetSDK.realtime.leaveRoom(room)— Resolves:{ room }DavidnetSDK.realtime.getRoomInfo(room)— checks how many members are currently in a room WITHOUT joining it, e.g. to show "3/8 players" on a lobby list before committing. Resolves:{ room, memberCount, members }DavidnetSDK.realtime.send(room, data, options?)— broadcasts any JSON-serializabledatato everyone else currently inroom(max ~64kb). Pass{ echo: true }to also receive your own message back viaonMessage. Fire-and-forget — it resolves once sent, it does not wait for delivery, so call it as often as your game needs (every input tick is fine — just not every render frame on top of that, see below).DavidnetSDK.realtime.setState(room, key, value)— sets a named key's value for everyone in the room: the server remembers the LATEST value per key and hands the full set back asstateto anyone who joins afterwards, plus pushes a livestateevent to everyone else already in the room. Useful for anything a late joiner needs to catch up on — player positions, ready/not-ready status, a shared scoreboard. You must be joined to the room first. Resolves:{ room, key }
// Shared room state: late joiners get the current snapshot for free via joinRoom's
// "state", and everyone already in the room gets a live update via onState.
async function joinArena(room) {
const { members, state } = await window.DavidnetSDK.realtime.joinRoom(room);
renderPlayerList(members);
Object.entries(state).forEach(([key, value]) => applyPlayerState(key, value));
window.DavidnetSDK.realtime.onState(({ key, value }) => applyPlayerState(key, value));
window.DavidnetSDK.realtime.onPresence(({ event, member }) => {
if (event === "join") addPlayerToList(member);
else removePlayerFromList(member.userId);
});
}
function onMyPositionChanged(room, myUserId, position) {
window.DavidnetSDK.realtime.setState(room, myUserId, position);
}
async function leaveArena(room) {
await window.DavidnetSDK.realtime.leaveRoom(room);
}Sending messages
// Fires for every message sent to any room you're in.
window.DavidnetSDK.realtime.onMessage(({ room, data, from }) => {
if (room !== currentRoom) return;
applyOpponentMove(data, from); // from = { userId, username, displayName, avatarUrl }
});
function sendMove(room, move) {
// Fire-and-forget - don't await every single call in a hot loop.
window.DavidnetSDK.realtime.send(room, { type: "move", move });
}
function sendMoveAndSeeMyOwn(room, move) {
// { echo: true } also delivers it back to yourself via onMessage - useful if
// your rendering code treats "my move" and "opponent move" the same way.
window.DavidnetSDK.realtime.send(room, { type: "move", move }, { echo: true });
}Matchmaking queues
DavidnetSDK.realtime.joinQueue(queue, groupSize, metadata?)— a matchmaking primitive. Everyone who callsjoinQueuewith the samequeuename should pass the samegroupSize. As soon asgroupSizecallers are waiting, the server pops them off in join order and auto-creates a room for them — listen for it withonMatched. Usemetadata(e.g. skill rating) if you want to build your own smarter matching on top of this — the server itself does plain FIFO grouping. Resolves:{ queue, position }DavidnetSDK.realtime.leaveQueue(queue)— Resolves:{ queue }DavidnetSDK.realtime.getQueueInfo(queue)— checks how many players are currently waiting in a queue WITHOUT joining it, e.g. to show "waiting for 2 more players" up front. Resolves:{ queue, waiting }DavidnetSDK.realtime.onMatched(callback)— fires once your queue found a full group:{ queue, room, members }. You're already joined toroomat this point — start callingsend(room, ...)/ listening withonMessageright away.
// A 1v1 ranked queue. The exact same calls work for a 50-player shooter lobby by
// passing a bigger groupSize - the server doesn't care how many players end up
// in a room, it just pops "groupSize" waiting callers off in join order.
let currentRoom = null;
async function findOpponent() {
showMessage("Searching for an opponent...");
await window.DavidnetSDK.realtime.joinQueue("ranked-1v1", 2);
}
window.DavidnetSDK.realtime.onMatched(({ room, members }) => {
currentRoom = room;
showMessage("Opponent found: " + members.find((m) => m.userId !== myUserId).displayName);
});
async function cancelSearch() {
await window.DavidnetSDK.realtime.leaveQueue("ranked-1v1");
}
// Show "waiting for N more players" on a lobby screen before joining.
async function showQueueStatus() {
const { waiting } = await window.DavidnetSDK.realtime.getQueueInfo("ranked-1v1");
showMessage(waiting + " player(s) currently waiting");
}Lobby-wide announcements
DavidnetSDK.realtime.announce(data, options?)— broadcasts to EVERY player currently connected to this game's lobby, not just a specific room — for server-wide-feeling announcements independent of whatever room each player is in. Pass{ echo: true }to also receive your own announcement back. Fire-and-forget. Resolves:{}DavidnetSDK.realtime.onAnnouncement(callback)— fires for every lobby-wide announcement from any connected player of this game:{ data, from, ts }.
// "Player X just beat the boss!" style feed, visible to everyone in the lobby
// regardless of which room (if any) they're currently in.
window.DavidnetSDK.realtime.onAnnouncement(({ data, from }) => {
if (data.type === "boss-defeated") {
showToast(from.displayName + " just beat the boss!");
}
});
async function onBossDefeated() {
await window.DavidnetSDK.realtime.announce({ type: "boss-defeated" });
}Connection lifecycle
DavidnetSDK.realtime.onDisconnect(callback) / onReconnect(callback) / onError(callback) — the connection reconnects automatically in the background and silently rejoins your rooms. onDisconnect fires when the connection drops, onReconnect fires after it's restored (with the rooms that were rejoined, so you can resync game state), and onError fires for server-side errors not tied to a specific call (e.g. rate-limited, or sent to a room you're
not in): { code, message }
window.DavidnetSDK.realtime.onDisconnect(() => {
showConnectionBanner("Reconnecting...");
});
window.DavidnetSDK.realtime.onReconnect(({ rooms }) => {
hideConnectionBanner();
// "rooms" were silently rejoined - you may still want to re-sync game state,
// e.g. by re-requesting the authoritative host's latest state.
requestStateResync(rooms);
});
window.DavidnetSDK.realtime.onError(({ code, message }) => {
console.warn("Realtime error:", code, message);
});Realtime's own rate limit
Realtime has its own separate rate limit — 200 messages per second per connection, enforced
over the WebSocket itself. This is unrelated to the per-player-per-game budget described in Rate limits , which only covers highscores/saves/achievements/levels — DavidnetSDK.getRateLimitStatus() does NOT reflect this realtime limit. 200/s is far more than any browser game needs for input sync
— most games are fine sending whenever game state actually changes (e.g. 10-30 times a second),
not on every animation frame.
Anti-cheat: the server is a dumb relay
For realtime games, treat the server as a dumb relay: it does not validate move legality, game rules, or physics. If your game needs to be cheat-resistant, have one client act as an authoritative host (e.g. whoever created the room) and treat other players' messages as input suggestions, not trusted state — same tradeoff as the anti-cheat model used for highscores.