CollabCanvas
Building CollabCanvas: A Real-Time Collaborative Drawing App from Scratch
When I set out to build CollabCanvas, I wanted to create something that would challenge me across the entire full-stack development spectrum. What started as a simple "whiteboard app" idea evolved into a comprehensive project that taught me everything from low-level canvas manipulation to real-time system architecture.
The Vision Behind CollabCanvas
CollabCanvas is a real-time collaborative drawing application where multiple users can join a shared canvas, draw together, and see each other's changes instantly. Think of it as a simplified version of tools like Figma or Miro, but built entirely from scratch to understand the underlying mechanics that power modern collaborative software.
The core concept was simple: enable multiple people to draw on the same canvas simultaneously, with every stroke, shape, and modification appearing in real-time across all connected devices. However, implementing this seemingly simple idea required solving complex challenges around state synchronization, real-time communication, and canvas performance optimization.
Technical Architecture Overview
I structured the project as a monorepo with three main components, each handling distinct responsibilities while maintaining seamless integration:
Project Structure
apps/
collabcanvas-frontend/ → React frontend with custom canvas engine
http-backend/ → Express API server for authentication and data
ws-backend/ → WebSocket server for real-time communication
packages/
ui/ → Shared React components
common/ → Shared TypeScript types and validation schemas
This separation allowed me to handle different concerns independently while maintaining code reusability and type safety across the entire application. The monorepo structure using Turborepo enabled efficient development workflows and shared dependencies.
The Frontend: Custom Canvas Implementation
Building Drawing Tools from Scratch
Rather than using existing drawing libraries like Fabric.js or Konva, I implemented all the drawing logic myself using HTML5 Canvas APIs. This decision gave me deep understanding of how canvas rendering works and complete control over the drawing experience, though it required significantly more development time.
Core Drawing Engine Architecture
The heart of the frontend is a custom Game class that encapsulates all canvas logic and real-time synchronization. This class-based approach provided clean encapsulation and made the complex drawing system manageable.
Shape System Design
type Shape =
| { type: "rect"; x: number; y: number; width: number; height: number }
| { type: "circle"; centerX: number; centerY: number; radius: number }
| { type: "pencil"; startX: number; startY: number; endX: number; endY: number };
Key Features:
• Each shape type has specific rendering logic and interaction patterns
• Rectangles and circles provide live preview during drawing
• Pencil tool creates immediate, permanent strokes for natural drawing feel
Enhanced Canvas Rendering System
The rendering system includes sophisticated styling and optimized drawing performance. Every frame completely redraws the canvas, ensuring perfect consistency across all users despite being computationally intensive:
clearCanvas() {
// Clear and prepare canvas with consistent styling
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.fillStyle = "rgba(0,0,0,1)";
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.strokeStyle = "rgba(255,255,255,1)";
this.ctx.lineWidth = 2;
// Render all existing shapes with optimized drawing calls
this.existingShapes.forEach((shape) => {
if (shape.type === "rect") {
this.ctx.strokeRect(shape.x, shape.y, shape.width, shape.height);
} else if (shape.type === "circle") {
this.ctx.beginPath();
this.ctx.arc(shape.centerX, shape.centerY, shape.radius, 0, Math.PI * 2);
this.ctx.stroke();
} else if (shape.type === "pencil") {
this.ctx.beginPath();
this.ctx.moveTo(shape.startX, shape.startY);
this.ctx.lineTo(shape.endX, shape.endY);
this.ctx.stroke();
}
});
}
Interactive Drawing Implementation
The drawing system handles three distinct interaction states with different behaviors for each tool type:
■ Mouse Down Handler: Initiates drawing action and captures starting coordinates for all tools.
■ Mouse Move Handler: Provides tool-specific behavior during active drawing:
- Pencil Tool: Creates immediate line segments and broadcasts each stroke for real-time collaboration
- Rectangle/Circle Tools: Shows live preview by redrawing canvas with temporary shape overlay
■ Mouse Up Handler: Completes drawing action, adds final shape to permanent collection, and broadcasts to other users.
Technical Note: The pencil tool required special handling to balance responsiveness with network efficiency. Each mouse movement creates a new line segment that's immediately drawn and broadcast, creating smooth, natural drawing experience while maintaining real-time synchronization.
Advanced Geometric Calculations
The circle drawing implementation uses enhanced geometry for more intuitive user experience:
// Euclidean distance calculation for natural circle sizing
const radius = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2)) / 2;
const centerX = this.startX + width / 2;
const centerY = this.startY + height / 2;
This approach makes circle drawing feel more natural compared to simple width/height calculations, as the radius expands based on diagonal mouse movement rather than rectangular bounds.
State Management and Persistence Strategy
The frontend manages multiple layers of state to ensure smooth collaboration and data persistence:
Dual-Layer Persistence System
• Local Storage Integration: Provides immediate access to previous drawings and offline capability:
// Store canvas state locally for quick access
localStorage.setItem(`canvas-${this.roomId}`, JSON.stringify(this.existingShapes));
// Retrieve and validate stored canvas data
const local = localStorage.getItem(`canvas-${this.roomId}`);
if (local) {
try {
const parsed = JSON.parse(local);
if (Array.isArray(parsed)) this.existingShapes = parsed;
} catch {
console.warn("Invalid canvas data in localStorage.");
}
}
• WebSocket Synchronization: Handles real-time updates and ensures consistency across all connected users.
Real-Time Synchronization Logic
The synchronization system handles multiple message types for different collaboration scenarios:
• New Shape Broadcasting
Purpose: Users create shapes
Implementation: Immediately added to local state and broadcast to room participants
• User Join Synchronization
Purpose: New users join active sessions
Implementation: Existing users send complete canvas state to newcomers
• Canvas History Distribution
Purpose: Server maintains authoritative history
Implementation: Distributed to users on request
This multi-layered approach ensures users always see consistent canvas state while maintaining responsive local interaction.
The Backend: Dual-Server Architecture
HTTP Server for Core Operations
The Express.js server handles traditional web operations with comprehensive TypeScript implementation. This server manages user authentication, room creation, and data persistence using a clean, RESTful API design.
Authentication System Implementation
• JWT-Based Security: Secure token-based authentication with proper expiration handling and refresh capabilities. The system generates tokens on successful login and validates them on protected routes.
• Input Validation: Comprehensive request validation using shared Zod schemas ensures type consistency and prevents malformed data from reaching the database:
// Shared validation schemas used across frontend and backend
export const CreateUserSchema = z.object({
email: z.string().email(),
password: z.string().min(6),
name: z.string().min(1),
photo: z.string().optional(),
});
export const CreateRoomSchema = z.object({
name: z.string().min(1),
});
• Protected Route Middleware: Authentication middleware validates JWT tokens and attaches user information to requests, enabling secure access to user-specific resources.
Room Management System
Key Features:
• Rooms use slug-based identifiers instead of UUIDs
• Users can create rooms like "/room/my-project" or "/room/team-brainstorm"
• Each room has a designated admin (creator) with future permission systems
• Graceful conflict resolution with clear error messages
WebSocket Server for Real-Time Magic
The WebSocket server represents the most technically challenging component, requiring careful management of connections, rooms, and state synchronization across multiple concurrent users.
Connection Management and Authentication
• JWT Authentication Integration: WebSocket connections authenticate using JWT tokens passed as query parameters, leveraging the existing authentication system:
function checkUser(token: string): string | null {
try {
const decoded = jwt.verify(token, JWT_SECRET) as JwtPayload;
if (!decoded || !decoded.userId) return null;
return decoded.userId;
} catch {
return null;
}
}
• User Session Tracking: Each connection maintains comprehensive user state including room membership, socket identification, and authentication status:
interface User {
ws: WebSocket;
rooms: string[];
userId: string;
socketId: string;
}
This structure enables efficient message routing and proper cleanup when users disconnect.
Room-Based Broadcasting System
• Efficient Message Routing: Drawing actions and messages are broadcast only to users in the same room, ensuring privacy and optimal performance. The server maintains room membership lists for fast message distribution.
• User Presence Management: The system tracks when users join and leave rooms, notifying other participants of presence changes. This enables features like user counts and activity indicators.
• Memory Management: Automatic cleanup of disconnected users prevents memory leaks and maintains accurate room membership lists.
Canvas History and Synchronization
• Database-Backed History: All drawing actions are persisted to PostgreSQL, ensuring canvas state survives server restarts and provides authoritative history for new users.
• Unified Message System: Both text messages and drawing actions are stored as "messages", with drawing data serialized as JSON. This unified approach simplifies storage and maintains chronological order of all room activity.
• New User Onboarding: When users join active drawing sessions, the server retrieves complete canvas history from the database and sends it as a single comprehensive update.
Key Technical Challenges and Solutions
Challenge 1: WebSocket Authentication and Security
• Problem: Securing WebSocket connections while maintaining the existing JWT authentication system.
• Solution: Implemented JWT token validation in the WebSocket connection handshake, using query parameters to pass tokens from authenticated HTTP sessions to WebSocket connections. This approach maintains security while enabling seamless real-time features.
Challenge 2: State Synchronization Across Multiple Clients
• Problem: Keeping all users synchronized with identical canvas state despite network latency and connection instability.
• Solution: Implemented a multi-layered synchronization system:
- Immediate Local Updates: Drawing actions appear instantly for the acting user
- Broadcast Distribution: Actions are immediately sent to all room participants
- Authoritative History: Server maintains complete, ordered history in database
- State Recovery: New users receive complete canvas history on joining
Challenge 3: Dual-Purpose Message Architecture
• Problem: Managing both text messages and drawing actions efficiently while maintaining chronological order.
• Solution: Designed a unified message system where drawing actions are stored as JSON-serialized messages. This approach provides:
Architectural Benefits:
• Single Storage System
Both message types use the same database table
• Chronological Consistency
All room activity maintains proper timeline order
• Simplified Recovery
Canvas reconstruction is just filtering and parsing messages
• Unified Broadcasting
WebSocket server handles all message types consistently
Challenge 4: Canvas Performance Optimization
• Problem: Maintaining smooth drawing experience while handling real-time synchronization and complex rendering.
• Solution: Implemented several optimization strategies:
• Shape-Based Rendering: Each drawing action creates discrete, serializable objects
• Full Canvas Redraws: Ensures perfect consistency despite computational cost
• Immediate Local Feedback: Users see their actions instantly before network synchronization
• Efficient Network Protocol: Minimal message overhead with JSON serialization
Challenge 5: Type Safety Across Full Stack
• Problem: Maintaining type consistency between frontend, backend, and database operations.
• Solution: Created shared package architecture with Prisma-generated types and Zod validation schemas:
- Compile-Time Safety: TypeScript types flow from database to frontend
- Runtime Validation: Shared Zod schemas prevent invalid data transmission
- Single Source of Truth: Database schema drives all type definitions
- Development Efficiency: Type errors caught at compile time rather than runtime
Database Design and Architecture
PostgreSQL Schema with Prisma
The database design elegantly handles both user management and the innovative dual-purpose message system using a clean, normalized schema that balances simplicity with functionality.
Core Models and Relationships
• User Model: Handles authentication and profile information with UUID primary keys for security and scalability. The optional photo field enables future avatar features without requiring schema migrations.
• Room Model: Uses human-readable slugs for better user experience while maintaining unique constraints. Each room tracks its creator (admin) and creation timestamp for management purposes.
• Message Model: The most innovative aspect of the schema, serving dual purpose for both text messages and JSON-serialized drawing actions. This unified approach provides several architectural benefits:
Advanced Design Decisions
• Relationship Architecture: The schema establishes clear ownership patterns with proper foreign key constraints:
- Users create and own rooms (one-to-many relationship)
- Users send messages and create drawings (one-to-many with Message)
- Rooms contain all their historical activity (one-to-many with Message)
• Type Integration: Prisma generates TypeScript types that flow through the entire application stack, ensuring compile-time safety from database operations to frontend rendering. This integration prevents runtime type errors and makes refactoring safer.
• Scalability Considerations: The current schema supports horizontal scaling with proper indexing on foreign keys and frequently queried fields. Room-based partitioning could be implemented for massive scale.
Performance Optimizations and Scalability
Frontend Performance Strategies
• Canvas Rendering Optimization: While full canvas redraws are computationally expensive, they ensure perfect visual consistency across all users. Future optimizations could include dirty region tracking and incremental updates.
• Memory Management: Proper event listener cleanup prevents memory leaks during component unmounting. The Game class includes comprehensive cleanup methods for production stability.
• Network Efficiency: Drawing actions are immediately applied locally before network transmission, providing responsive user feedback regardless of network conditions.
Backend Scalability Architecture
• Stateless HTTP Design: The Express server maintains no user state, enabling horizontal scaling with load balancers. All session data is stored in JWT tokens or the database.
• WebSocket Connection Management: Current implementation uses in-memory user tracking, suitable for moderate scale. Redis integration would enable multi-server WebSocket deployments.
• Database Performance: Proper indexing on foreign keys and frequently queried fields ensures fast queries. The message table could benefit from time-based partitioning for large-scale deployments.
Development Workflow and Tools
Monorepo Architecture Benefits
• Turborepo Integration: Efficient build caching and parallel execution across all packages and applications. The monorepo structure enabled rapid development cycles with shared dependencies.
• Shared Package System: Common types, validation schemas, and utilities prevent code duplication while ensuring consistency. Changes to shared packages automatically propagate to all consumers.
• Development Environment: Hot reloading across frontend and backend with proper dependency tracking. The development experience remained smooth despite the complex multi-service architecture.
Type Safety Implementation
• End-to-End Types
Implementation: Database schema generates TypeScript types that flow through Prisma client to API routes to frontend components
Benefit: Catches type mismatches at compile time rather than runtime
• Validation Layer
Implementation: Zod schemas provide runtime validation that matches TypeScript compile-time types
Benefit: Ensures data integrity at application boundaries
• Error Handling
Implementation: Comprehensive error types and handling throughout the application stack
Benefit: Proper HTTP status codes and user-friendly error messages
Lessons Learned and Technical Insights
Architecture Decisions
• Custom vs. Library Trade-offs: Building the canvas engine from scratch provided deep learning but required significant development time. Understanding when to build custom solutions versus using existing libraries is crucial for project success.
• Real-Time System Complexity: WebSocket implementation revealed the intricate challenges of distributed state management. Proper error handling, connection lifecycle management, and state synchronization require careful planning.
• Database Design Impact: The dual-purpose message system design decision simplified implementation while maintaining flexibility. Creative schema design can elegantly solve complex requirements.
Development Practices
• Incremental Development: Starting with basic functionality and adding complexity gradually prevented overwhelming technical debt. Each feature built naturally on previous implementations.
• Performance Considerations: Balancing user experience with technical constraints required constant evaluation. Some performance sacrifices (like full canvas redraws) were acceptable for correctness guarantees.
Technical Growth
Future Enhancement Roadmap
Immediate Technical Improvements
• Canvas History Management: Implement undo/redo functionality with command pattern architecture. This requires careful state management and efficient storage of drawing operations.
• User Presence Indicators: Show real-time cursor positions and active users with WebSocket presence broadcasting. This enhances collaboration awareness and user engagement.
• Performance Optimizations: Implement canvas viewport culling and dirty region tracking to improve rendering performance for large drawings.
Advanced Features
• Layer Management System: Add support for drawing layers with opacity controls and blend modes. This requires significant canvas architecture changes but enables professional-grade functionality.
• Advanced Drawing Tools: Implement text annotations, eraser functionality, and shape modification tools. Each tool requires careful integration with the real-time synchronization system.
Scalability Enhancements
• Redis Integration: Implement Redis for session management and WebSocket connection sharing across multiple server instances. This enables horizontal scaling of real-time features.
Conclusion and Reflection
Building CollabCanvas from scratch was an extraordinary learning journey that provided deep insights into modern web application architecture. Every component, from low-level canvas manipulation to distributed system design, presented unique challenges that expanded my technical capabilities.
Key Takeaways
• Foundational Understanding: Implementing features from scratch, rather than using existing libraries, provided invaluable understanding of underlying mechanics. This knowledge makes me more effective when using high-level tools and better equipped to debug complex issues.
• System Design Skills: Architecting real-time collaborative features required careful consideration of state management, network protocols, and user experience. These system design skills apply broadly to many technical challenges.
• Full-Stack Proficiency: Building authentication, real-time communication, database design, and interactive frontend features in a single project provided comprehensive experience across the entire development stack.
Technical Philosophy
The project reinforced that some of the most valuable learning happens when you avoid the convenience of existing solutions and build things from fundamental principles. While production applications should leverage established libraries and frameworks, understanding the underlying mechanisms makes you a significantly better developer.
Future Applications
The skills developed during this project - real-time system architecture, canvas manipulation, WebSocket implementation, and collaborative software design - apply to numerous domains including gaming, design tools, educational platforms, and productivity software.
Building CollabCanvas demonstrated that complex, professional-grade applications are achievable with careful planning, incremental development, and persistence through technical challenges. The experience provides a strong foundation for tackling even more ambitious projects in the future.
Explore the project: github.com/ombalgude/collabcanvas
Interested in the technical implementation details? I'd love to discuss the architectural decisions, challenges encountered, and solutions developed throughout this project. Feel free to reach out with questions or to share your own experiences building collaborative real-time applications!