This site is not affiliated with or endorsed by Cloudflare, Inc. It simply showcases experiments built using Cloudflare services.
Cloudflare Experiments
Stateful & Async

Live Cursor Tracker

Real-time shared cursor positions over WebSocket using Durable Objects and the Hibernation API

Broadcast cursor positions to every client in a shared room using a Durable Object and the WebSocket Hibernation API. Open multiple tabs to / and move your mouse to see colored cursors sync in real time.

Features

  • GET / - Demo page with canvas and WebSocket client
  • GET /ws/:room - WebSocket upgrade; one DO instance per room name
  • Hibernation API - acceptWebSocket, webSocketMessage, webSocketClose
  • Colored cursors - Each client gets a stable color and guest label

API Reference

GET /

Returns an HTML demo page with inline JavaScript that connects to /ws/:room.

Open http://localhost:8787/ in two browser tabs to test locally.

GET /ws/:room

Upgrades to WebSocket and joins the room's Durable Object.

room string (path, required)

Room name: letters, numbers, _, or - (max 64 characters).

Client messages (JSON)

Send cursor position updates:

{ "x": 120, "y": 340 }

Server messages (JSON)

{ "type": "join", "id": "uuid", "color": "#3b82f6", "name": "Guest-a1b2" }
{ "type": "cursor", "id": "uuid", "x": 120, "y": 340, "color": "#3b82f6", "name": "Guest-a1b2" }
{ "type": "leave", "id": "uuid", "color": "#3b82f6", "name": "Guest-a1b2" }

Error Codes

  • 400 - Invalid room name (INVALID_ROOM)
  • 426 - Missing WebSocket upgrade header

Use Cases

  • Learn Durable Object WebSocket hibernation patterns
  • Prototype collaborative UI features (cursors, presence, cursors-on-canvas)
  • Demo real-time broadcast without an external pub/sub service

Limitations

  • No authentication; any client can join any room name
  • Cursor state is ephemeral (not persisted to storage)
  • Demo page only; production apps would use a custom frontend

Use in your project

Copy these files into an existing Worker. Prefer Deployment to try the full experiment first. Source: apps/experiments/live-cursor-tracker.

DependenciesNoneBindingsDurable ObjectsPlatformWebSockets, Durable Objects
export const DEMO_HTML = `<!DOCTYPE html><html lang="en"><head>  <meta charset="utf-8" />  <meta name="viewport" content="width=device-width, initial-scale=1" />  <title>Live Cursor Tracker</title>  <style>    * { box-sizing: border-box; margin: 0; padding: 0; }    body { font-family: system-ui, sans-serif; background: #0f172a; color: #e2e8f0; min-height: 100vh; }    header { padding: 1rem 1.5rem; border-bottom: 1px solid #334155; display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; }    #room { background: #1e293b; border: 1px solid #475569; border-radius: 6px; padding: 0.35rem 0.6rem; color: inherit; width: 10rem; }    #status { font-size: 0.875rem; color: #94a3b8; }    #canvas { position: relative; width: 100%; height: calc(100vh - 64px); overflow: hidden; cursor: crosshair; }    .cursor { position: absolute; pointer-events: none; transform: translate(-50%, -50%); transition: left 80ms linear, top 80ms linear; }    .cursor-label { font-size: 11px; padding: 2px 6px; border-radius: 4px; white-space: nowrap; margin-top: 4px; }    .dot { width: 12px; height: 12px; border-radius: 50%; border: 2px solid #fff; margin: 0 auto; }  </style></head><body>  <header>    <strong>Live Cursor Tracker</strong>    <label>Room <input id="room" value="demo" /></label>    <button id="connect">Connect</button>    <span id="status">Disconnected</span>  </header>  <div id="canvas"></div>  <script>    const canvas = document.getElementById('canvas');    const statusEl = document.getElementById('status');    const roomInput = document.getElementById('room');    const connectBtn = document.getElementById('connect');    const peers = new Map();    let ws = null;    let selfId = null;    function renderPeer(id, data) {      let el = peers.get(id);      if (!el) {        el = document.createElement('div');        el.className = 'cursor';        el.innerHTML = '<div class="dot"></div><div class="cursor-label"></div>';        canvas.appendChild(el);        peers.set(id, el);      }      el.style.left = data.x + 'px';      el.style.top = data.y + 'px';      el.querySelector('.dot').style.background = data.color;      el.querySelector('.cursor-label').textContent = data.name || id.slice(0, 6);      el.querySelector('.cursor-label').style.background = data.color;    }    function removePeer(id) {      const el = peers.get(id);      if (el) { el.remove(); peers.delete(id); }    }    function connect() {      if (ws) ws.close();      const room = roomInput.value.trim() || 'demo';      const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';      ws = new WebSocket(protocol + '//' + location.host + '/ws/' + encodeURIComponent(room));      ws.onopen = () => { statusEl.textContent = 'Connected to room: ' + room; };      ws.onclose = () => { statusEl.textContent = 'Disconnected'; };      ws.onmessage = (event) => {        const msg = JSON.parse(event.data);        if (msg.type === 'join') {          if (!selfId) selfId = msg.id;          renderPeer(msg.id, { x: 40, y: 40, color: msg.color, name: msg.name });        } else if (msg.type === 'cursor') {          renderPeer(msg.id, msg);        } else if (msg.type === 'leave') {          removePeer(msg.id);        }      };      canvas.onmousemove = (e) => {        if (!ws || ws.readyState !== WebSocket.OPEN) return;        const rect = canvas.getBoundingClientRect();        ws.send(JSON.stringify({ x: e.clientX - rect.left, y: e.clientY - rect.top }));      };    }    connectBtn.onclick = connect;    connect();  </script></body></html>`;

Deployment

Click the deploy button

Deploy to Cloudflare Workers

Deploy

Durable Object migration is included in wrangler.json. No additional bindings required.

Local Development

cd apps/experiments/live-cursor-tracker
npm install
npm run dev

Open http://localhost:8787/ in two tabs.

Configuration

BindingPurpose
ROOMSDurable Object namespace (CursorRoom class)

Cloudflare Features Used

  • Workers - Edge compute runtime
  • Durable Objects - Room-scoped WebSocket coordination
  • WebSocket Hibernation API - Efficient long-lived connections

On this page