Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

29 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Collaborative Paint

Real-time collaborative drawing demonstrating plugin-oriented architecture and screaming architecture principles


🎯 What This Project Demonstrates

This isn't just a drawing appβ€”it's an architectural showcase:

  1. βœ… TRUE Plugin Architecture - Extension points where multiple independent modules compose
  2. βœ… Screaming Architecture - File names immediately tell you what they do
  3. βœ… Single Responsibility - Each module has ONE clear purpose
  4. βœ… TypeScript - Strong typing makes plugin contracts explicit
  5. ❌ NOT Just - Swappable implementations (Strategy Pattern / Dependency Injection)

The Key Distinction

// ❌ This is NOT plugin architecture (it's Strategy Pattern)
const storage = useRedis ? new RedisStorage() : new MemoryStorage();

// βœ… This IS plugin architecture (composable hooks)
middleware.use('stroke:before', validateStroke);    // Plugin 1
middleware.use('stroke:before', rateLimitCheck);    // Plugin 2
middleware.use('stroke:after', logStroke);          // Plugin 3
// Multiple plugins compose without knowing about each other

πŸš€ Quick Start

# Install dependencies
npm install

# Build TypeScript
npm run build

# Start the server
npm start

# Open browser
open http://localhost:3000

Usage:

  1. Click "Create Session" to start a new drawing session
  2. Share the session link with others to collaborate
  3. Draw in real-time with multiple users
  4. Try switching tools (Pen/Eraser) and colors

πŸ—οΈ Architecture Overview

Project Structure (Self-Documenting)

collaborative-drawing/
β”œβ”€β”€ client/
β”‚   β”œβ”€β”€ index.html                  # 🎨 UI
β”‚   β”œβ”€β”€ main.ts                     # 🎬 App orchestrator  
β”‚   β”œβ”€β”€ core/
β”‚   β”‚   β”œβ”€β”€ CanvasController.ts     # πŸ–ΌοΈ  Tool-agnostic canvas
β”‚   β”‚   β”œβ”€β”€ SessionManager.ts       # 🎫 Session lifecycle
β”‚   β”‚   β”œβ”€β”€ ToolRegistry.ts         # ⭐ Tool plugin system
β”‚   β”‚   β”œβ”€β”€ UIController.ts         # πŸŽ›οΈ  UI interactions
β”‚   β”‚   └── WebSocketClient.ts      # πŸ”Œ Connection management
β”‚   └── tools/                      
β”‚       β”œβ”€β”€ BaseTool.ts             # πŸ”§ Tool interface
β”‚       β”œβ”€β”€ EraserTool.ts           # 🧹 Eraser tool plugin
β”‚       └── PenTool.ts              # ✏️  Pen tool plugin
β”‚
β”œβ”€β”€ server/
β”‚   β”œβ”€β”€ index.ts                    # 🎬 Server entry point
β”‚   β”œβ”€β”€ routes.ts                   # πŸ›£οΈ  HTTP routes
β”‚   β”œβ”€β”€ core/
β”‚   β”‚   β”œβ”€β”€ MiddlewareManager.ts    # ⭐ Hook system (plugin architecture)
β”‚   β”‚   └── SessionStore.ts         # πŸ’Ύ Session state management
β”‚   β”œβ”€β”€ handlers/
β”‚   β”‚   └── SocketHandlers.ts       # πŸ”Œ WebSocket event handlers
β”‚   β”œβ”€β”€ plugins/                  
β”‚   β”‚   β”œβ”€β”€ logStroke.ts            # πŸ“ Logging middleware
β”‚   β”‚   β”œβ”€β”€ validateStroke.ts       # βœ… Validation middleware
β”‚   β”‚   └── rateLimitCheck.ts       # 🚦 Rate limiting middleware
β”‚   └── config/
β”‚       └── plugins.ts              # βš™οΈ  Plugin configuration
β”‚
└── shared/
    └── types.ts                    # πŸ“ TypeScript types

Key Observations:

  • Screaming Architecture: File names tell you EXACTLY what they do
  • Clear Separation: Plugins vs Core vs Handlers
  • Single Responsibility: Each file has ONE job

πŸ”Œ Two Plugin Systems (Fullstack)

1. Backend: Middleware Hook System

Extension Points:

  • stroke:before - Before broadcasting (validation, rate limiting)
  • stroke:after - After broadcasting (logging, webhooks)
  • user:join - When user connects
  • session:create - When session created

Current Plugins:

  • βœ… validateStroke - Validates stroke data
  • βœ… rateLimitCheck - Prevents spam/abuse
  • βœ… logStroke - Logs for analytics

Why This Works:

  • Multiple plugins can attach to same hook
  • Plugins execute in sequence (composable)
  • Adding webhook plugin = new file + config line
  • Core broadcast logic never changes

2. Frontend: Tool Registry System

Extension Point:

  • Tools register themselves via ToolRegistry
  • Canvas controller is tool-agnostic
  • Tools extend BaseTool interface

Current Tools:

  • βœ… PenTool - Freehand drawing
  • βœ… EraserTool - Erasing strokes

Why This Works:

  • Canvas controller doesn't know about specific tools
  • Registry pattern for discovery
  • Each tool is self-contained module
  • Adding rectangle tool = new file + registration

🎨 Adding New Features (Demonstrating Plugin Architecture)

Example: Add Rectangle Tool

Time to implement: 30 minutes (if architecture is right)

// Step 1: Create client/tools/RectangleTool.ts
import BaseTool, { DrawContext } from './BaseTool.js';

export default class RectangleTool extends BaseTool {
  onMouseDown(point: Point, context: DrawContext): void { /* ... */ }
  onMouseMove(point: Point, context: DrawContext): void { /* ... */ }
  onMouseUp(point: Point, context: DrawContext): void { /* ... */ }
  render(ctx: CanvasRenderingContext2D, stroke: Stroke): void { /* ... */ }
}

// Step 2: Register in client/main.ts
this.toolRegistry.register('rectangle', RectangleTool);

// βœ… Done! CanvasController unchanged.

🧠 Key Design Decisions

1. Plugin Architecture vs Swappable Implementations

What I Built:

  • Middleware hook system where multiple plugins compose
  • Tool registry where tools register themselves
  • Extension points in the core

What I Didn't Build:

  • Simple interface swapping (that's Strategy Pattern)
  • Everything as a plugin (some things are infrastructure)

2. What IS a Plugin (Extensions)

βœ… Middleware: validation, rate limiting, logging, webhooks
βœ… Drawing Tools: pen, eraser, shapes, text
βœ… Effects: filters, transformations (future)
βœ… Integrations: Slack, analytics (future)

🎨 Technical Stack

Backend:

  • TypeScript (ES2020)
  • Node.js + Express
  • Socket.io (WebSocket with reconnection)
  • In-memory session storage

Frontend:

  • TypeScript (ES2020, ES Modules)
  • Vanilla JavaScript (no framework)
  • Native Canvas API
  • Socket.io client

Build System:

  • TypeScript compiler
  • Separate server/client builds
  • No bundler needed (ES modules)

πŸ”„ Real-Time Features

Reconnection Handling

Problem: User draws β†’ disconnects β†’ reconnects Solution:

  1. Client queues strokes when offline
  2. On reconnect: send lastKnownTimestamp
  3. Server replays missed strokes
  4. Client deduplicates by stroke ID
  5. Client sends queued strokes

Conflict Resolution

Problem: Out-of-order stroke delivery Solution: Server assigns authoritative timestamp; client sorts before rendering

Rate Limiting

Problem: User sends 1000 strokes/second Solution: Rate limit plugin stops excessive requests before broadcast


πŸ› Known Limitations

Out of Scope (Intentional):

  • ❌ Undo/redo system
  • ❌ Canvas zoom/pan
  • ❌ Image export
  • ❌ Cursor tracking
  • ❌ Authentication
  • ❌ Persistent storage
  • ❌ Automated tests

Reason: This is an architectural demonstration focused on plugin systems and real-time collaboration patterns. For production deployment, automated tests would be the first priority (unit tests for middleware execution, integration tests for WebSocket flows, E2E tests for multi-user scenarios).


πŸš€ Deployment

Deploy to Railway (Recommended)

Time: 5 minutes

Railway provides free hosting with full WebSocket support - perfect for this real-time collaborative app.

Option 1: CLI (Fastest)

# Install Railway CLI
npm i -g @railway/cli

# Login (opens browser)
railway login

# Initialize project
railway init

# Deploy
railway up

# Generate public URL
railway domain

You'll get a URL like: https://collaborative-paint.up.railway.app

Option 2: Web Dashboard (No CLI)

  1. Go to railway.app/new
  2. Click "Deploy from GitHub repo"
  3. Connect your GitHub account and select this repository
  4. Railway auto-detects railway.json configuration
  5. Click "Deploy"
  6. Go to Settings β†’ "Generate Domain"
  7. Done! Your app is live πŸŽ‰

Configuration

Railway uses railway.json:

{
  "build": {
    "builder": "NIXPACKS",
    "buildCommand": "npm install && npm run build"
  },
  "deploy": {
    "startCommand": "npm start"
  }
}

Nixpacks automatically:

  • Detects Node.js from package.json
  • Installs dependencies (including TypeScript)
  • Builds the application
  • Starts the server

Verify Deployment

After deploying, test:

  1. βœ… Create a session
  2. βœ… Share session link
  3. βœ… Open link in another browser/incognito window
  4. βœ… Draw in both windows simultaneously
  5. βœ… Verify real-time sync works

πŸ“„ License

ISC


About

🎨 Plugin architecture showcase

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages