Real-time collaborative drawing demonstrating plugin-oriented architecture and screaming architecture principles
This isn't just a drawing appβit's an architectural showcase:
- β TRUE Plugin Architecture - Extension points where multiple independent modules compose
- β Screaming Architecture - File names immediately tell you what they do
- β Single Responsibility - Each module has ONE clear purpose
- β TypeScript - Strong typing makes plugin contracts explicit
- β NOT Just - Swappable implementations (Strategy Pattern / Dependency Injection)
// β 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# Install dependencies
npm install
# Build TypeScript
npm run build
# Start the server
npm start
# Open browser
open http://localhost:3000Usage:
- Click "Create Session" to start a new drawing session
- Share the session link with others to collaborate
- Draw in real-time with multiple users
- Try switching tools (Pen/Eraser) and colors
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
Extension Points:
stroke:before- Before broadcasting (validation, rate limiting)stroke:after- After broadcasting (logging, webhooks)user:join- When user connectssession: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
Extension Point:
- Tools register themselves via
ToolRegistry - Canvas controller is tool-agnostic
- Tools extend
BaseToolinterface
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
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.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)
β
Middleware: validation, rate limiting, logging, webhooks
β
Drawing Tools: pen, eraser, shapes, text
β
Effects: filters, transformations (future)
β
Integrations: Slack, analytics (future)
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)
Problem: User draws β disconnects β reconnects Solution:
- Client queues strokes when offline
- On reconnect: send
lastKnownTimestamp - Server replays missed strokes
- Client deduplicates by stroke ID
- Client sends queued strokes
Problem: Out-of-order stroke delivery Solution: Server assigns authoritative timestamp; client sorts before rendering
Problem: User sends 1000 strokes/second Solution: Rate limit plugin stops excessive requests before broadcast
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).
Time: 5 minutes
Railway provides free hosting with full WebSocket support - perfect for this real-time collaborative app.
# Install Railway CLI
npm i -g @railway/cli
# Login (opens browser)
railway login
# Initialize project
railway init
# Deploy
railway up
# Generate public URL
railway domainYou'll get a URL like: https://collaborative-paint.up.railway.app
- Go to railway.app/new
- Click "Deploy from GitHub repo"
- Connect your GitHub account and select this repository
- Railway auto-detects
railway.jsonconfiguration - Click "Deploy"
- Go to Settings β "Generate Domain"
- Done! Your app is live π
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
After deploying, test:
- β Create a session
- β Share session link
- β Open link in another browser/incognito window
- β Draw in both windows simultaneously
- β Verify real-time sync works
ISC