Metaloot Multiplayer

Every game deployed with metaloot deploy ships with a multiplayer backend already provisioned: rooms, presence, message relay, and shared room state, served from Metaloot's edge on your game's own domain. There is nothing to host, no SDK to install, and no keys to manage — adding multiplayer later is a purely client-side change.

What you get on every deploy

  • Rooms — each room is its own isolated realtime instance at wss://<your-game>.metaloot.app/mp/rooms/<roomId>, created on demand the moment the first player joins. Rooms are namespaced per game, so your "lobby" never collides with another game's.
  • Presence— a live roster of who is in the room, with join/leave events carrying each player's stable Metaloot identity.
  • Message relay— send JSON to everyone else in the room, or to one player, with low latency from Cloudflare's edge.
  • Shared room state — a small key-value store every client sees in the same authoritative order; perfect for match phase, scores, or a shared RNG seed.
  • Zero-install client — your site serves the client module itself at /__metaloot/multiplayer.js, versioned with the platform.

Metaloot auth is integral

Rooms only accept signed-in Metaloot players. Because the room WebSocket lives on your game's own origin, the session cookie set by Sign in with Metaloot authenticates the connection automatically — no tokens in client code, no extra keys. Every message and presence event carries a verified, stable Metaloot player id, so identity, display names, and avatars work in multiplayer out of the box. On Metaloot hosting the auth endpoints and sign-in widget are provisioned by the same deploy, so the only thing your game has to handle is the signed-out case: joinRoom() throws a MetalootAuthRequiredError whose signIn() sends the player to log in.

Agent Instructions

Paste this into your coding agent to add multiplayer to a game deployed with metaloot deploy. Replace the placeholder URL with your game's *.metaloot.app address — the prompt covers the Metaloot sign-in requirement too, since rooms only accept signed-in players.

Add Metaloot multiplayer to this game. The game is deployed on Metaloot hosting (https://your-game.metaloot.app via `metaloot deploy`), which already provides the full multiplayer backend — rooms, presence, message relay, and shared room state — at wss://your-game.metaloot.app/mp/rooms/<roomId>. Do NOT add any server, backend service, or third-party realtime SDK; the Metaloot infrastructure is already provisioned.

Metaloot auth is an integral part of multiplayer: the room WebSocket is authenticated by the player's Metaloot session cookie (same origin), so players must sign in with Metaloot before they can join a room.
- On Metaloot hosting, auth already works with zero code: /auth/metaloot/start, /auth/metaloot/session, and /auth/metaloot/logout run at the edge, and a "Sign in with Metaloot" widget is auto-injected (window.metaloot has { session, signIn, signOut }).
- Docs: https://metaloot.app/docs/auth and https://metaloot.app/docs/multiplayer

Implementation requirements:
1. Use the zero-install client that Metaloot hosting serves on the game's own origin — do not install an npm package for this:
   import { joinRoom, MetalootAuthRequiredError } from "/__metaloot/multiplayer.js";
2. Gate multiplayer on sign-in. joinRoom() throws MetalootAuthRequiredError when the player is signed out; catch it and show a "Sign in to play online" button that calls error.signIn() (or window.metaloot.signIn()). Never try to bypass auth or hand-roll the WebSocket connection.
3. Join a room and wire the game to it:
   const room = await joinRoom("lobby");            // room ids: 1-64 chars of A-Za-z0-9 _ . ~ -
   room.self                                        // { connectionId, id, name, imageUrl } — id is the stable Metaloot player id
   room.players                                     // other connections currently in the room
   room.on("join",  (player) => { ... });
   room.on("leave", (player) => { ... });
   room.on("message", ({ from, data }) => { ... }); // data is whatever another client sent
   room.send({ kind: "move", x, y });               // relay to everyone else (or room.send(data, playerId) for one player)
   room.setState("phase", "playing");               // small shared key-value state, echoed to all clients in one authoritative order
   room.on("state", ({ key, value, from }) => { ... });
   room.on("reconnect", ({ players, state }) => { ... }); // resync UI after an automatic reconnect
   room.leave();
4. Pick room ids that fit the game: a fixed id like "lobby" for a shared space, or a generated code the host shares for private matches.
5. Architecture: the relay is not an authoritative server. Send small, semantic messages (inputs/events, not full world snapshots each frame) and make clients converge on shared state via room.setState for the few values that must agree (match phase, scores, seed).
6. Limits to respect: 32 connections per room, 32 KB per message, room state up to 256 keys; room state is cleared when the last player leaves. Messages are JSON.
7. Keep a single-player path working when the player is signed out or the connection drops (the client auto-reconnects and emits "reconnect", and emits "close" after it gives up).
8. Verify on the deployed site (https://your-game.metaloot.app): sign in with Metaloot, open the game in two browser windows, and confirm both see each other join, exchange messages, and see the same shared state.

Quick Start

On a deployed game, this is the entire integration — no build step or package required:

import { joinRoom, MetalootAuthRequiredError } from "/__metaloot/multiplayer.js";

let room;
try {
  room = await joinRoom("lobby");
} catch (error) {
  if (error instanceof MetalootAuthRequiredError) {
    showSignInButton(() => error.signIn()); // players must sign in with Metaloot
  }
  throw error;
}

console.log("me:", room.self);       // { connectionId, id, name, imageUrl }
console.log("others:", room.players);

room.on("join", (player) => addPlayer(player));
room.on("leave", (player) => removePlayer(player));
room.on("message", ({ from, data }) => handleInput(from, data));

room.send({ kind: "move", x: 3, y: 7 }); // relayed to everyone else

Shared state that all clients agree on (updates are echoed back to everyone, including the sender, in one authoritative order):

room.setState("phase", "playing");
room.on("state", ({ key, value }) => {
  if (key === "phase") setPhase(value);
});
console.log(room.state); // live snapshot, kept in sync

Client Reference

joinRoom(roomId?, options?) → Promise<Room> — connects to a room (default "lobby"). Room ids are 1–64 characters of letters, digits and - _ . ~. Use a fixed id for a shared space or a generated code for private matches.

room.self / room.players — your identity and the other connections. player.id is the stable Metaloot player id (the same one Metaloot auth reports); player.connectionId distinguishes two tabs of the same player.

room.send(data, to?) — relay JSON to everyone else, or to one player id / connection id. room.setState(key, value) — update shared state (pass null to delete a key). room.leave() — disconnect.

room.on(event, handler) — events: join, leave, message, state, reconnect, close, error. The client pings automatically and reconnects with backoff; after a reconnect, refresh your UI from the reconnect payload (fresh roster and state).

Design Notes & Limits

  • The room is a relay plus shared state, not an authoritative game server. Send small semantic messages (inputs and events, not per-frame world snapshots) and put the few values that must agree — match phase, scores, seed — in room state.
  • Limits: 32 connections per room, 32 KB per JSON message, 256 shared-state keys. Room state is cleared when the last player leaves — rooms are play sessions, not databases. For durable saves, keep using your game's own storage keyed by the Metaloot player id.
  • Multiplayer runs on Cloudflare Durable Objects, colocated with your game's hosting — rooms spin up on demand and cost you nothing. Dedicated authoritative game servers (server-side simulation) are on the roadmap as a separate tier.
  • Multiplayer is available on Metaloot-hosted games (*.metaloot.app). If your game runs elsewhere, deploy it with npx @metaloot/cli deploy first — hosting, auth, and multiplayer are provisioned by the same command.