Ra1NuX.

How to Build a Multiplayer Game Almost Without a Server (With Yjs + WebRTC)

Cover Image for How to Build a Multiplayer Game Almost Without a Server (With Yjs + WebRTC)
Raimundo Martinez nuñez
Raimundo Martinez nuñez

If you've ever dreamed of making a multiplayer game that runs without needing a backend, this post is for you. We'll explore how to use Yjs and WebRTC to synchronize game state between players — peer to peer, with almost zero infrastructure.

🧠 What Does "Almost Without a Server" Mean?

WebRTC enables direct communication between browsers. However, to establish that connection, the players must first exchange network information (called signaling), usually through a small WebSocket server. The good news? That’s all the server does. No database, no game logic, just a minimal relay.

🔧 Tools Used

  • Yjs – CRDT-based shared state
  • y-webrtc – Yjs provider over WebRTC
  • A tiny WebSocket server (or use a public one for testing)

🚀 How It Works

Instead of sending each movement over the network, players synchronize their state using a shared CRDT document. Every player holds the entire state of the game (e.g., character positions), and changes are automatically merged and broadcast.


// Initialize Yjs doc and WebRTC provider
import * as Y from 'yjs';
import { WebrtcProvider } from 'y-webrtc';

const ydoc = new Y.Doc();
const provider = new WebrtcProvider('my-vtt-room', ydoc, {
  signaling: ['wss://your-signaling-server.com'] // Use your own or public
});

// Shared map of characters
const characters = ydoc.getMap('characters');

// Move a character
function moveCharacter(id, q, r) {
  characters.set(id, { q, r }); // Full object replacement = observable
}

// Listen for changes
characters.observe(event => {
  event.changes.keys.forEach((change, id) => {
    const updated = characters.get(id);
    animateCharacterTo(id, updated.q, updated.r);
  });
});
  

🔥 Why This Works

  • Peer to peer: WebRTC connects browsers directly, so latency is low.
  • No central game server: State is shared and conflict-free using Yjs.
  • Simple to integrate: Works in plain HTML, WebView, or React.
  • Offline-capable (optionally): Yjs can be extended with persistence.

🧪 Gotchas

When using Yjs, it's critical to update state using its API. For example:


// ❌ This won't trigger observers!
const token = characters.get('knight');
token.q = 5; // silent fail

// ✅ This triggers observers and syncs across peers
characters.set('knight', { q: 5, r: 3 });
  

📦 What About the Signaling Server?

You can use Yjs’s public servers, but for production, hosting a tiny signaling server is easy. Here’s one in Node.js using ws:


// signaling-server.js
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 4444 });

wss.on('connection', ws => {
  ws.on('message', msg => {
    wss.clients.forEach(client => {
      if (client !== ws && client.readyState === ws.OPEN) {
        client.send(msg);
      }
    });
  });
});
  

✅ TL;DR

  • You can build multiplayer without a real server using WebRTC and Yjs.
  • You just need a signaling server for the handshake (no game logic, no database).
  • Use Y.Map.set instead of mutating objects directly to trigger syncs and animations.