Building a Collaborative Drawing App with Reactive Programming

After diving into reactive programming basics and building a real-time dashboard, let's take things up a notch! Today we're going to build something super fun - a drawing app where multiple people can draw on the same canvas at the same time.

Remember when you were a kid and would fight with your siblings over who gets to color which part of the drawing? Well, this app solves that problem - everyone can draw at once!

The complete source code for this tutorial is available in my GitHub repository.

What We're Building#

We'll create a collaborative drawing app where:

  • Multiple users can draw on the same canvas at the same time
  • Everyone sees what others are drawing in real-time
  • Each person gets their own color
  • You can see who's currently online and drawing
  • The drawing persists if you reload the page

This project showcases some powerful reactive programming concepts:

  1. Using WebSockets to share drawing events between users
  2. Handling mouse movements as streams of events
  3. Broadcasting state changes to all connected clients
  4. Synchronizing the canvas state for new users who join

Don't worry if that sounds like a lot - I'll walk you through it step by step!

Prerequisites#

Before we start, you should:

  • Know JavaScript basics and ES6
  • Have a basic understanding of reactive programming (observables, subscribers)
  • Have gone through the reactive programming introduction
  • Have Node.js and npm installed

If you're brand new to reactive programming, check out the other posts in this series first.

Project Setup#

Let's create our project structure:

code8 lines
1drawing-app/
2  ├── package.json
3  ├── server.js
4  ├── public/
5  │   ├── index.html
6  │   ├── style.css
7  │   └── app.js
8  └── .gitignore

First, make a new project folder and set things up:

bash5 lines
1mkdir drawing-app
2cd drawing-app
3npm init -y
4npm install express socket.io rxjs
5npm install --save-dev nodemon

We need both a server and a client, so Express and Socket.IO will help us with the WebSocket communication between users.

Basic Server (server.js)#

Let's create a simple server that will host our app and handle WebSocket connections:

js93 lines
1const express = require("express");
2const http = require("http");
3const socketIo = require("socket.io");
4const path = require("path");
5
6// Create our Express app, HTTP server, and Socket.io server
7const app = express();
8const server = http.createServer(app);
9const io = socketIo(server);
10
11// Serve static files from the public directory
12app.use(express.static(path.join(__dirname, "public")));
13
14// Keep track of all drawing points to replay for new users
15let drawHistory = [];
16// Keep track of connected users
17let users = {};
18
19// When a client connects
20io.on("connection", (socket) => {
21  console.log("A user connected:", socket.id);
22
23  // Create a random color for this user
24  const color = getRandomColor();
25  users[socket.id] = {
26    id: socket.id,
27    color,
28    isDrawing: false,
29  };
30
31  // Send the current state to the new user
32  socket.emit("init", {
33    drawHistory,
34    users,
35  });
36
37  // Let everyone know about the new user
38  io.emit("userJoined", users[socket.id]);
39
40  // When user sends draw data
41  socket.on("draw", (data) => {
42    data.color = users[socket.id].color;
43    data.userId = socket.id;
44
45    // Store in history (so new users can see existing drawing)
46    drawHistory.push(data);
47
48    // Keep history from getting too big
49    if (drawHistory.length > 1000) {
50      drawHistory = drawHistory.slice(drawHistory.length - 1000);
51    }
52
53    // Send to all OTHER clients (broadcasting)
54    socket.broadcast.emit("draw", data);
55  });
56
57  // When user starts/stops drawing
58  socket.on("drawingState", (isDrawing) => {
59    users[socket.id].isDrawing = isDrawing;
60    io.emit("userStateChanged", { userId: socket.id, isDrawing });
61  });
62
63  // Handle clear canvas request
64  socket.on("clearCanvas", () => {
65    // Clear the drawing history
66    drawHistory = [];
67    // Broadcast to all clients to clear their canvases
68    io.emit("clearCanvas");
69  });
70
71  // When user disconnects
72  socket.on("disconnect", () => {
73    console.log("User disconnected:", socket.id);
74    delete users[socket.id];
75    io.emit("userLeft", socket.id);
76  });
77});
78
79// Start server
80const PORT = process.env.PORT || 3000;
81server.listen(PORT, () => {
82  console.log(`Server running on port ${PORT}`);
83});
84
85// Helper to generate random colors
86function getRandomColor() {
87  const letters = "0123456789ABCDEF";
88  let color = "#";
89  for (let i = 0; i < 6; i++) {
90    color += letters[Math.floor(Math.random() * 16)];
91  }
92  return color;
93}

The server code is pretty straightforward:

  1. We set up Socket.IO to handle WebSocket connections
  2. We track drawing history and connected users
  3. For each new user, we send them the current state and give them a random color
  4. We handle drawing events and broadcast them to other users
  5. We manage when users connect, disconnect, and change drawing state

HTML Structure (public/index.html)#

Next, let's create the front-end interface:

html38 lines
1<!DOCTYPE html>
2<html lang="en">
3  <head>
4    <meta charset="UTF-8" />
5    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6    <title>Collaborative Drawing App</title>
7    <link rel="stylesheet" href="style.css" />
8  </head>
9  <body>
10    <div class="container">
11      <div class="header">
12        <h1>Let's Draw Together!</h1>
13        <div class="tools">
14          <button id="clear-btn">Clear Canvas</button>
15          <div class="user-list">
16            <h3>Who's Drawing:</h3>
17            <ul id="users"></ul>
18          </div>
19        </div>
20      </div>
21
22      <div class="canvas-container">
23        <canvas id="drawing-canvas"></canvas>
24      </div>
25
26      <div class="footer">
27        <p>Your color: <span id="my-color">loading...</span></p>
28        <p class="help-text">
29          Start drawing by clicking and dragging on the canvas
30        </p>
31      </div>
32    </div>
33
34    <script src="/socket.io/socket.io.js"></script>
35    <script src="https://unpkg.com/rxjs@7/dist/bundles/rxjs.umd.min.js"></script>
36    <script src="app.js"></script>
37  </body>
38</html>

CSS Styling (public/style.css)#

Let's add some basic styling:

css114 lines
1* {
2  box-sizing: border-box;
3  margin: 0;
4  padding: 0;
5}
6
7body {
8  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
9    Arial, sans-serif;
10  line-height: 1.6;
11  color: #333;
12  background-color: #f5f5f5;
13}
14
15.container {
16  max-width: 1200px;
17  margin: 0 auto;
18  padding: 1rem;
19}
20
21.header {
22  display: flex;
23  justify-content: space-between;
24  align-items: center;
25  margin-bottom: 1rem;
26  flex-wrap: wrap;
27}
28
29h1 {
30  margin-bottom: 1rem;
31  color: #2c3e50;
32}
33
34.tools {
35  display: flex;
36  gap: 1rem;
37  align-items: start;
38}
39
40button {
41  padding: 0.5rem 1rem;
42  background-color: #3498db;
43  color: white;
44  border: none;
45  border-radius: 4px;
46  cursor: pointer;
47  font-size: 1rem;
48}
49
50button:hover {
51  background-color: #2980b9;
52}
53
54.canvas-container {
55  background-color: white;
56  border-radius: 8px;
57  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
58  overflow: hidden;
59}
60
61#drawing-canvas {
62  display: block;
63  background-color: white;
64  cursor: crosshair;
65  width: 100%;
66  height: 600px;
67}
68
69.footer {
70  display: flex;
71  justify-content: space-between;
72  margin-top: 1rem;
73  font-size: 0.9rem;
74  color: #7f8c8d;
75}
76
77.user-list {
78  background: white;
79  border-radius: 4px;
80  padding: 0.5rem 1rem;
81  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
82  min-width: 150px;
83}
84
85.user-list h3 {
86  margin-bottom: 0.5rem;
87  font-size: 1rem;
88}
89
90.user-list ul {
91  list-style-type: none;
92}
93
94.user-list li {
95  margin-bottom: 0.5rem;
96  display: flex;
97  align-items: center;
98}
99
100.user-color {
101  display: inline-block;
102  width: 12px;
103  height: 12px;
104  border-radius: 50%;
105  margin-right: 8px;
106}
107
108.user-drawing {
109  font-weight: bold;
110}
111
112#my-color {
113  font-weight: bold;
114}

The Client-Side App#

Now comes the fun part - let's build the reactive drawing app!

Reactive Drawing Logic (public/app.js)#

js179 lines
1// Get RxJS from the UMD bundle
2const { fromEvent, merge } = rxjs;
3const { map, switchMap, takeUntil, tap, filter, share, pairwise, startWith } =
4  rxjs.operators;
5
6// Set up canvas and context
7const canvas = document.getElementById("drawing-canvas");
8const context = canvas.getContext("2d");
9const usersElement = document.getElementById("users");
10const clearButton = document.getElementById("clear-btn");
11const myColorElement = document.getElementById("my-color");
12
13// Socket.io is loaded from the server
14const socket = io();
15
16// Set the canvas size to match its display size
17function setupCanvas() {
18  const rect = canvas.getBoundingClientRect();
19  canvas.width = rect.width;
20  canvas.height = rect.height;
21  context.lineCap = "round";
22  context.lineJoin = "round";
23  context.lineWidth = 3;
24}
25
26// Set up the canvas when page loads and when window resizes
27window.addEventListener("load", setupCanvas);
28window.addEventListener("resize", setupCanvas);
29
30// Keep track of our user
31let myColor;
32let myUserId;
33let users = {};
34
35// Process drawing events from the server
36socket.on("draw", drawLine);
37
38// Process initial state when connecting
39socket.on("init", (data) => {
40  // Save user info
41  myUserId = socket.id;
42  myColor = data.users[myUserId].color;
43  users = data.users;
44
45  // Display my color
46  myColorElement.textContent = myColor;
47  myColorElement.style.color = myColor;
48
49  // Draw the existing drawing
50  data.drawHistory.forEach(drawLine);
51
52  // Update the user list
53  updateUserList();
54});
55
56// Handle user joining
57socket.on("userJoined", (user) => {
58  users[user.id] = user;
59  updateUserList();
60});
61
62// Handle user leaving
63socket.on("userLeft", (userId) => {
64  delete users[userId];
65  updateUserList();
66});
67
68// Handle user state change (drawing or not)
69socket.on("userStateChanged", (data) => {
70  users[data.userId].isDrawing = data.isDrawing;
71  updateUserList();
72});
73
74// Clear button handler
75clearButton.addEventListener("click", () => {
76  // Clear locally
77  context.clearRect(0, 0, canvas.width, canvas.height);
78  // Tell server to clear for everyone
79  socket.emit("clearCanvas");
80});
81
82// Server tells us to clear
83socket.on("clearCanvas", () => {
84  context.clearRect(0, 0, canvas.width, canvas.height);
85});
86
87// Draw a line segment based on coordinates
88function drawLine(data) {
89  context.strokeStyle = data.color;
90  context.beginPath();
91  context.moveTo(data.prevX, data.prevY);
92  context.lineTo(data.currX, data.currY);
93  context.stroke();
94}
95
96// Update the user list display
97function updateUserList() {
98  usersElement.innerHTML = "";
99  Object.values(users).forEach((user) => {
100    const li = document.createElement("li");
101    li.innerHTML = `
102      <span class="user-color" style="background-color: ${user.color}"></span>
103      <span class="${user.isDrawing ? "user-drawing" : ""}">${
104      user.id === myUserId ? "You" : `User ${user.id.substr(0, 5)}`
105    }</span>
106      ${user.isDrawing ? " (drawing)" : ""}
107    `;
108    usersElement.appendChild(li);
109  });
110}
111
112// Now for the reactive part!
113// Let's create observables from mouse events on the canvas
114const mouseMove$ = fromEvent(canvas, "mousemove");
115const mouseDown$ = fromEvent(canvas, "mousedown");
116const mouseUp$ = fromEvent(canvas, "mouseup");
117const mouseLeave$ = fromEvent(canvas, "mouseleave");
118
119// This is where reactive programming really shines for drawing apps
120// We'll track mouse movements while the mouse is down
121const drawing$ = mouseDown$.pipe(
122  tap(() => {
123    // Tell everyone we're drawing
124    socket.emit("drawingState", true);
125  }),
126  // switchMap lets us switch to a new observable sequence
127  switchMap((down) => {
128    // Get the starting position
129    const startX = down.offsetX;
130    const startY = down.offsetY;
131
132    // Create the initial point
133    const startPoint = {
134      offsetX: startX,
135      offsetY: startY,
136    };
137
138    // Return the movements until mouse up or leave
139    return mouseMove$.pipe(
140      // Start with the initial point to connect properly
141      startWith(startPoint),
142      // Use pairwise to get the previous and current points
143      pairwise(),
144      // Map to the format our drawing function expects
145      map(([prev, curr]) => ({
146        prevX: prev.offsetX,
147        prevY: prev.offsetY,
148        currX: curr.offsetX,
149        currY: curr.offsetY,
150      })),
151      // Stop tracking when mouse up or leave occurs
152      takeUntil(merge(mouseUp$, mouseLeave$))
153    );
154  }),
155  // This is important! Share makes this a hot observable
156  // so multiple subscribers don't recreate the events
157  share()
158);
159
160// When we finish drawing
161merge(mouseUp$, mouseLeave$).subscribe(() => {
162  socket.emit("drawingState", false);
163});
164
165// Subscribe to the drawing stream
166drawing$.subscribe((coords) => {
167  // Draw locally
168  drawLine({
169    ...coords,
170    color: myColor,
171  });
172
173  // Emit to the server
174  socket.emit("draw", coords);
175});
176
177// Debug when we connect/disconnect
178socket.on("connect", () => console.log("Connected to server"));
179socket.on("disconnect", () => console.log("Disconnected from server"));

Understanding The Reactive Drawing Logic#

Let's break down the cool parts of our reactive drawing app:

1. Event Streams#

We're turning mouse events into observables:

js4 lines
1const mouseMove$ = fromEvent(canvas, "mousemove");
2const mouseDown$ = fromEvent(canvas, "mousedown");
3const mouseUp$ = fromEvent(canvas, "mouseup");
4const mouseLeave$ = fromEvent(canvas, "mouseleave");

These are streams of events that we can transform and combine using RxJS operators. This is much cleaner than nesting traditional event listeners.

2. Drawing as a Stream#

The magic happens in the drawing$ observable:

js20 lines
1const drawing$ = mouseDown$.pipe(
2  tap(() => {
3    socket.emit("drawingState", true);
4  }),
5  switchMap((down) => {
6    const startX = down.offsetX;
7    const startY = down.offsetY;
8
9    return mouseMove$.pipe(
10      map((move) => ({
11        prevX: startX,
12        prevY: startY,
13        currX: move.offsetX,
14        currY: move.offsetY,
15      })),
16      takeUntil(merge(mouseUp$, mouseLeave$))
17    );
18  }),
19  share()
20);

Let's break this down in simpler terms:

  • When you press the mouse button (mouseDown$), we start paying attention to where your mouse moves
  • We capture each movement and calculate the line that should be drawn
  • We keep doing this until you release the mouse or move it off the canvas
  • share() makes sure we don't duplicate the events for multiple subscribers

This way, drawing becomes a single stream of line segments, where each segment connects where your mouse was to where it is now.

3. Multicasting with Socket.IO#

When we subscribe to the drawing stream, we both draw locally AND send the line data to other users:

js10 lines
1drawing$.subscribe((coords) => {
2  // Draw locally
3  drawLine({
4    ...coords,
5    color: myColor,
6  });
7
8  // Emit to the server
9  socket.emit("draw", coords);
10});

The server then broadcasts this to all other connected clients:

js3 lines
1socket.on("draw", (data) => {
2  socket.broadcast.emit("draw", data);
3});

This creates a synchronized drawing experience. When you draw something, everyone else sees it immediately.

The Power of Sharing State#

In our app, there are two main types of state that get shared:

  1. Drawing State - The actual lines people draw
  2. User State - Who's connected and whether they're actively drawing

Let me walk you through how each part works.

Sharing Drawing State#

When someone draws, we:

  1. Capture their mouse movements as line segments using RxJS
  2. Draw those segments locally on their canvas
  3. Send the segment data to the server via Socket.IO
  4. The server broadcasts to all other clients
  5. Other clients receive the data and draw the same segment

This direct sharing of drawing actions (not just the final picture) means everyone can see the drawing happen stroke by stroke. It's much more interactive than just sharing a final image!

Sharing User State#

We also keep track of:

  • Who's currently connected (with their unique colors)
  • Who's actively drawing right now

This information gets updated in real-time. When someone starts drawing, everyone else can see that they're active.

Syncing New Users#

When a new person joins, we don't want them to see a blank canvas! So:

  1. We store drawing history on the server
  2. When someone new connects, we send them the full drawing history
  3. Their canvas gets filled in with all existing lines
  4. Then they start receiving real-time updates

This creates a seamless experience - you can join at any time and still see what everyone has drawn so far.

Testing The App#

Let's run the app to see it in action:

  1. Add this start script to package.json:
json4 lines
1"scripts": {
2  "start": "node server.js",
3  "dev": "nodemon server.js"
4}
  1. Start the development server:
bash1 lines
1npm run dev
  1. Open http://localhost:3000 in your browser

  2. For the full experience, open the app in multiple browser windows and watch as drawings sync between them!

Troubleshooting Common Issues#

1. "Why aren't the lines connecting smoothly?"#

When drawing, you might notice gaps between line segments. This happens because we're only capturing mouse movements at certain intervals. To fix this, change how we draw lines:

js32 lines
1// Original drawing function
2function drawLine(data) {
3  context.strokeStyle = data.color;
4  context.beginPath();
5  context.moveTo(data.prevX, data.prevY);
6  context.lineTo(data.currX, data.currY);
7  context.stroke();
8}
9
10// Improved version that tracks the last position
11let lastX = null;
12let lastY = null;
13
14function drawLine(data) {
15  context.strokeStyle = data.color;
16  context.beginPath();
17
18  // If this is part of an ongoing drawing, connect from the last point
19  if (lastX && data.userId === lastUserId) {
20    context.moveTo(lastX, lastY);
21  } else {
22    context.moveTo(data.prevX, data.prevY);
23  }
24
25  context.lineTo(data.currX, data.currY);
26  context.stroke();
27
28  // Remember the last position and user
29  lastX = data.currX;
30  lastY = data.currY;
31  lastUserId = data.userId;
32}

2. "The app slows down after drawing a lot"#

The canvas can get sluggish when there's too much drawing history. Two fixes:

  1. Limit history on the server (we already do this)
  2. Periodically convert the canvas to an image:
js18 lines
1// Take a snapshot every 100 drawing actions
2let drawCount = 0;
3
4drawing$.subscribe((coords) => {
5  drawCount++;
6
7  if (drawCount % 100 === 0) {
8    takeCanvasSnapshot();
9  }
10});
11
12function takeCanvasSnapshot() {
13  // Temporarily store the canvas data
14  const imageData = canvas.toDataURL("image/png");
15
16  // Optionally send to server to update baseline for new users
17  socket.emit("canvasSnapshot", imageData);
18}

3. "Users don't see all drawing actions"#

WebSockets sometimes miss messages if there's network lag. Add acknowledgments and retries:

js15 lines
1// When sending drawing data
2socket.emit("draw", coords, (ack) => {
3  if (!ack) {
4    // If no acknowledgment, try again
5    socket.emit("draw", coords);
6  }
7});
8
9// On the server
10socket.on("draw", (data, callback) => {
11  // Process the data...
12
13  // Send acknowledgment
14  if (callback) callback(true);
15});

Real-World Improvements#

Here are some ways to make this app even better:

1. Drawing Tools#

Add options for:

  • Different brush sizes
  • Shapes (circles, rectangles)
  • An eraser tool
  • Text tool
js20 lines
1// Example of implementing brush size
2const brushSize = document.getElementById("brush-size");
3
4brushSize.addEventListener("change", (e) => {
5  context.lineWidth = e.target.value;
6  socket.emit("updateSettings", { lineWidth: e.target.value });
7});
8
9socket.on("updateSettings", (settings) => {
10  if (settings.userId !== socket.id) {
11    userSettings[settings.userId] = settings;
12  }
13});
14
15// Then in drawLine():
16function drawLine(data) {
17  context.strokeStyle = data.color;
18  context.lineWidth = userSettings[data.userId]?.lineWidth || 3;
19  // Rest of drawing logic...
20}

2. Rooms & Private Drawing Spaces#

Let users create private drawing rooms:

js27 lines
1// On the client
2const roomButton = document.getElementById("create-room");
3
4roomButton.addEventListener("click", () => {
5  const roomName = prompt("Enter a room name:");
6  if (roomName) {
7    socket.emit("joinRoom", roomName);
8  }
9});
10
11// On the server
12socket.on("joinRoom", (roomName) => {
13  // Leave current rooms
14  Object.keys(socket.rooms).forEach((room) => {
15    if (room !== socket.id) socket.leave(room);
16  });
17
18  // Join new room
19  socket.join(roomName);
20
21  // Get room history
22  const roomHistory = drawHistoryByRoom[roomName] || [];
23  socket.emit("roomJoined", { room: roomName, history: roomHistory });
24
25  // Broadcast to room members only
26  socket.to(roomName).emit("userJoined", users[socket.id]);
27});

3. Undo/Redo Functionality#

Add undo/redo support:

js37 lines
1// Track drawing actions in arrays
2let drawActions = [];
3let redoStack = [];
4
5drawing$.subscribe((coords) => {
6  // Add to actions stack
7  drawActions.push({
8    type: "draw",
9    data: { ...coords, color: myColor },
10  });
11
12  // Clear redo stack when new drawing happens
13  redoStack = [];
14
15  // Draw and emit as before
16  // ...
17});
18
19document.getElementById("undo-btn").addEventListener("click", () => {
20  if (drawActions.length === 0) return;
21
22  // Move latest action to redo stack
23  const lastAction = drawActions.pop();
24  redoStack.push(lastAction);
25
26  // Redraw everything from scratch
27  redrawCanvas();
28
29  // Tell others
30  socket.emit("undo");
31});
32
33socket.on("undo", (userId) => {
34  // Handle others' undos
35  // ...
36  redrawCanvas();
37});

The Magic Behind Multiuser Drawing#

The real magic of this app isn't just in the drawing - it's in synchronizing state between users in real-time. Let's look at what makes this possible:

1. Shared Observable Execution#

With traditional event listeners, each listener runs independently. But with RxJS, we can create a pipeline where events flow through transformations and can be shared between multiple subscribers.

The share() operator is key here - it turns a "cold" observable (which runs separately for each subscriber) into a "hot" observable (which runs once and broadcasts to all subscribers).

2. Stateful vs. Stateless Communication#

Our app uses both patterns:

Stateful (the canvas itself):

  • We keep the entire drawing history on the server
  • New users get the full history to catch up

Stateless (the drawing actions):

  • Each drawing action is independent
  • Actions flow from user → server → other users without requiring context

This hybrid approach gives us the best of both worlds: full state for new users, and fast, lightweight updates for ongoing drawing.

3. Eventual Consistency#

What happens if messages arrive out of order? In our app, it doesn't matter much! Each line segment is drawn where it should be, regardless of when it arrives.

This is called "eventual consistency" - even if there are temporary network delays, everyone's canvas will eventually look the same.

Key Takeaways#

As you build apps like this collaborative drawing tool, keep these concepts in mind:

  1. Think in streams - Mouse movements, drawing actions, and user state are all streams of events

  2. Share resources efficiently - Use share() to avoid duplicate processing and prevent multiple WebSocket connections

  3. Design for real-time sync - Send small, frequent updates rather than large batches

  4. Handle new users gracefully - Make sure people joining late can catch up with the current state

  5. Plan for network issues - Add retry logic and fallbacks for when connections drop

  6. Separate UI updates from data logic - Reactive programming makes this natural

References & Resources#

Official Documentation#

Tutorials & Articles#

Libraries & Tools#

  • RxJS - The reactive extensions library for JavaScript
  • Socket.IO - Real-time bidirectional event-based communication
  • Fabric.js - Canvas library with more advanced drawing tools

Example Projects#

Final Thoughts#

Building a collaborative drawing app with reactive programming shows how powerful this approach can be for real-time, interactive applications. The beauty is in how clean and maintainable the code stays, even as complexity grows.

The real win here is that we've created something that would be pretty complicated with traditional approaches, but becomes straightforward when thinking in terms of streams and transformations.

I hope this tutorial has shown you not just how to build a drawing app, but how reactive programming can tackle real-world problems in an elegant way.

Next time you're building something where multiple users need to interact in real-time, remember the patterns we've used here. Whether it's a collaborative text editor, a multiplayer game, or a shared dashboard - reactive programming has you covered!

Happy coding (and drawing)!

MB

Mehrshad Baqerzadegan

Sharing thoughts on technology and best practices.