Imagine you are in a design review meeting. Your team is spread across three time zones. Someone shares their screen, but you cannot draw directly on it. You type feedback into a chat box, and the context gets lost. A real-time collaborative whiteboard fixes that. It lets everyone draw, sketch, and annotate together, in the same browser window, at the same moment. In this guide, you will learn how to build one using the Canvas API for drawing and WebSockets for live communication. We will walk through the architecture, the code, and the tricky parts like conflict resolution.
This tutorial provides a complete blueprint for building a real-time collaborative whiteboard with websockets and the HTML5 Canvas API. You will learn how to set up a WebSocket server, broadcast drawing events, manage user cursors, and handle concurrent edits. The result is a working prototype you can extend with undo, room management, and more.
Understanding the Core Architecture
A collaborative whiteboard needs two main parts. The first is the client side, which handles user input and renders strokes on a <canvas> element. The second is the server side, which uses WebSockets to relay drawing events to every other connected client.
Client responsibilities:
- Capture mouse or touch events.
- Convert those events into stroke data (points, color, width).
- Send that data to the server as a JSON message.
- Receive messages from the server and draw them on the local canvas.
Server responsibilities:
- Maintain a set of active WebSocket connections.
- Receive a drawing message from one client.
- Broadcast that message to all other clients in the same room.
This pattern is simple but powerful. The server does not store the entire canvas state. It acts as a message relay. Each client is responsible for applying incoming strokes to its own canvas.
Setting Up the WebSocket Server
We will use Node.js with the ws library. It is lightweight and works well for this use case. If you prefer a different stack, the same logic applies to any WebSocket implementation.
- Create a new project folder and initialize it.
- Install the
wslibrary. - Write a basic server that listens for connections and broadcasts messages.
Here is a minimal server example:
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => {
ws.on('message', (data) => {
// Broadcast the message to every other client
wss.clients.forEach((client) => {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(data);
}
});
});
});
This server listens on port 8080. When it receives a message from one client, it sends that same message to all other connected clients. It does not modify the data. It does not store history. This keeps the server fast and stateless.
Building the Canvas Client
On the client side, you need an HTML page with a <canvas> element and some JavaScript to handle drawing.
Key steps for the client:
- Get a reference to the canvas and its drawing context.
- Set up event listeners for
mousedown,mousemove, andmouseup. - Track whether the user is currently drawing.
- On each mouse move while drawing, record the point and draw a line on the local canvas.
- Send the point data to the WebSocket server.
Here is a simplified drawing handler:
let isDrawing = false;
let currentStroke = [];
canvas.addEventListener('mousedown', (e) => {
isDrawing = true;
const point = { x: e.offsetX, y: e.offsetY };
currentStroke = [point];
ctx.beginPath();
ctx.moveTo(point.x, point.y);
});
canvas.addEventListener('mousemove', (e) => {
if (!isDrawing) return;
const point = { x: e.offsetX, y: e.offsetY };
currentStroke.push(point);
ctx.lineTo(point.x, point.y);
ctx.stroke();
});
canvas.addEventListener('mouseup', () => {
isDrawing = false;
ws.send(JSON.stringify({ type: 'stroke', points: currentStroke }));
});
This code sends the entire stroke as a JSON array of points after the mouse button is released. That reduces the number of messages sent over the wire compared to sending every single point individually.
Handling Real-Time Collaboration
When a client receives a stroke message from the server, it must draw that stroke on its own canvas. The receiving client does not know the drawing style of the remote user. You must include metadata like color and stroke width in the message.
Message format example:
{
"type": "stroke",
"color": "#ff0000",
"width": 3,
"points": [
{ "x": 10, "y": 20 },
{ "x": 15, "y": 25 }
]
}
The receiving client parses this message, sets the stroke color and width on its context, and draws the path.
Important consideration: Each client maintains its own canvas. If a new user joins a room mid-session, they will not see any strokes drawn before they connected. To solve this, you can add a state synchronization mechanism. The server can store the last N strokes and send them to newly connected clients. Alternatively, you can use a shared data structure like a list of strokes that each client requests on join.
Managing Multiple Users and Rooms
A whiteboard used by a single team needs room isolation. You do not want users from different projects seeing each other's drawings.
Room management approach:
- When a client connects, the server assigns them to a room based on a URL parameter or a handshake message.
- The server maintains a map of room IDs to sets of WebSocket connections.
- Broadcasts only go to connections within the same room.
Here is a modified server snippet with room support:
const rooms = new Map();
wss.on('connection', (ws) => {
let currentRoom = null;
ws.on('message', (data) => {
const msg = JSON.parse(data);
if (msg.type === 'join') {
currentRoom = msg.room;
if (!rooms.has(currentRoom)) {
rooms.set(currentRoom, new Set());
}
rooms.get(currentRoom).add(ws);
} else if (msg.type === 'stroke') {
// Broadcast to room
rooms.get(currentRoom).forEach((client) => {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(data);
}
});
}
});
});
This keeps the whiteboard sessions private. Each room acts like a separate virtual whiteboard.
Challenges and Common Mistakes
Building a real-time collaborative whiteboard is not without pitfalls. Here are the most common issues developers face and how to avoid them.
| Challenge | Mistake | Solution |
|---|---|---|
| Canvas scaling | Using absolute pixel coordinates without accounting for canvas size changes. | Store coordinates as percentages of canvas width and height. |
| Message flooding | Sending a WebSocket message on every mouse movement event. | Batch points into a single stroke message on mouse up. |
| Late joiners | New users see a blank canvas. | Implement a stroke history buffer on the server. |
| Cursor lag | Remote cursors appear jerky or delayed. | Send cursor position updates at a lower frequency (every 50ms). |
| Undo/redo | Users expect to undo their own strokes without affecting others. | Track per-user stroke lists and send undo commands as events. |
Expert advice: Always validate incoming stroke data on the server. A malicious client could send malformed JSON or an excessive number of points. Use a simple sanitizer that checks for expected fields and limits array sizes. This keeps your whiteboard running smoothly for everyone.
Adding User Cursors
Remote cursors make the collaboration feel more natural. Each user should see a small colored circle or name tag showing where other participants are pointing.
Implementation steps:
- Send cursor position updates from each client at a reduced rate (every 50 to 100 milliseconds).
- The server broadcasts these cursor positions to other users in the same room.
- On the receiving client, draw a small circle at the remote cursor position.
- Remove the cursor if no update is received for a few seconds (the user may have disconnected).
This feature adds a layer of presence that makes the whiteboard feel alive. It is especially useful during brainstorming sessions when someone is pointing at a specific area.
Performance Optimizations
A whiteboard with many users and long drawing sessions can become slow. Here are some ways to keep it responsive.
- Limit canvas redraws: Do not redraw the entire canvas on every message. Instead, draw incoming strokes directly onto the canvas as they arrive.
- Use a lower resolution for cursors: Cursor positions do not need pixel-perfect accuracy. Reduce the coordinate precision to two decimal places.
- Compress messages: For very active boards, consider using a binary protocol or compressing JSON payloads with a library like
zlib. - Offload rendering: If the canvas becomes too complex, use an offscreen canvas to composite strokes and copy the result to the visible canvas.
For a deeper look at how modern web tools can improve your workflow, check out this guide on top 10 web tools every developer should use in 2026.
Testing Your Whiteboard
Before deploying, test with multiple users in different browsers and devices. Here is a checklist.
- Open two browser windows side by side.
- Draw in one window and confirm the stroke appears in the other.
- Test with different stroke colors and widths.
- Join a room late and verify that previous strokes appear.
- Disconnect one client and confirm the other client continues working.
- Test on a mobile device with touch input.
Handling touch events requires mapping touchstart, touchmove, and touchend to the same drawing logic. The Canvas API works the same way, but you must account for multiple fingers and prevent default scrolling.
Extending the Whiteboard
Once the basic whiteboard works, you can add features that make it production ready.
- Undo and redo: Store each user's strokes in a list. When a user triggers undo, send a message that removes the last stroke from the shared state. Other clients apply the removal by redrawing all strokes except the removed one.
- Background images: Allow users to upload an image and set it as the canvas background. This is useful for annotating screenshots or diagrams.
- Text tool: Add the ability to place text boxes on the canvas. Each text box is another type of message that gets broadcast.
- Export: Provide a button to download the canvas as a PNG image using
canvas.toDataURL().
If you are interested in other real-time applications, you might enjoy this tutorial on how to build a real-time chat app with websockets and javascript. The same WebSocket patterns apply.
Security Considerations
WebSocket connections are not encrypted by default. For any production use, run your server over WSS (WebSocket Secure). This requires an SSL certificate, but it prevents eavesdropping on drawing data.
Also consider rate limiting. A single client should not be able to flood the server with messages. Add a simple token bucket or limit messages to a certain number per second.
Putting It All Together
You now have a clear path to build a real-time collaborative whiteboard with WebSockets and the Canvas API. Start with the server and a single client. Add room management. Then layer on cursors, undo, and export. Each step adds value without overcomplicating the code.
The real power of this setup is its simplicity. The server just relays messages. The clients handle all the rendering. This keeps the architecture easy to debug and scale. If you need to support hundreds of users in a single room, you can swap the simple broadcast for a more sophisticated message broker. But for most teams, the approach we covered here works perfectly.
Go ahead and build your own whiteboard. Get a small team to test it during a real meeting. You will see the difference it makes when everyone can draw together. And if you run into issues, remember that the canvas is just pixels and the WebSocket is just a pipe. Debug each layer separately. You have got this.