Skip to content

Latest commit

Β 

History

95 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Infinite Canvas

Real-time collaborative whiteboard with physics, offline sync, and time travel

real-time-canvas-web CI real-time-canvas-service CI

πŸ“‹ Overview

Infinite Canvas is a full-stack collaborative workspace that enables multiple users to create, manipulate, and interact with content on an infinite 2D surface in real-time. Built for hackathons and creative collaboration, it combines advanced whiteboard features with physics simulations, offline capabilities, and session replay.

The platform consists of two independently deployable apps, each with its own README:

There is no root package.json/workspace tooling tying them together β€” each has its own dependency graph and lockfile, and is built/tested independently (see CI below).

✨ Features

Core Features

  • Real-time Collaboration: Multi-user sessions with live cursor tracking and presence
  • Infinite Canvas: Smooth zoom, pan, and infinite scrolling with 100+ objects
  • Rich Content Tools: Text, shapes, images, sticky notes, and audio recording
  • Guest Authentication: Simple username-based login without sign-up requirements
  • Responsive Design: Works seamlessly across desktop and mobile

Advanced Technical Features

  • Physics Engine: Objects with gravity, collisions, attraction/repulsion (Matter.js)
  • Mini-map + Radar: Real-time location tracking of collaborators
  • Offline Support: Queue operations and sync when back online
  • Time Travel: Replay entire session from beginning
  • Export Options: PNG, SVG, and JSON formats

πŸ—οΈ System Architecture

Infinite Canvas system architecture diagram β€” client, backend, and data layers

ASCII reference diagrams (high-level architecture, frontend architecture, data flow)

High-Level Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                         Client (Browser)                           β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚                  Next.js Frontend (React 19)               β”‚  β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚  β”‚
β”‚  β”‚  β”‚  Canvas  β”‚ β”‚  Tools   β”‚ β”‚  Physics β”‚ β”‚  Time    β”‚    β”‚  β”‚
β”‚  β”‚  β”‚  Engine  β”‚ β”‚  Panel   β”‚ β”‚  Engine  β”‚ β”‚  Travel  β”‚    β”‚  β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚  β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚  β”‚
β”‚  β”‚  β”‚  Minimap β”‚ β”‚  Offline β”‚ β”‚  Export  β”‚ β”‚  Real-   β”‚    β”‚  β”‚
β”‚  β”‚  β”‚  + Radar β”‚ β”‚   Sync   β”‚ β”‚  Module  β”‚ β”‚  time    β”‚    β”‚  β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                              β”‚                                    β”‚
β”‚              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”‚
β”‚              β”‚                               β”‚                   β”‚
β”‚              β–Ό                               β–Ό                   β”‚
β”‚      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”               β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”‚
β”‚      β”‚  HTTP REST    β”‚               β”‚   WebSocket   β”‚          β”‚
β”‚      β”‚  (API Calls)  β”‚               β”‚   (Live Sync) β”‚          β”‚
β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜               β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚
                               β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Backend (Go + Gin)                              β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚                    API Gateway Layer                        β”‚  β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚  β”‚
β”‚  β”‚  β”‚   Auth   β”‚ β”‚  Rooms   β”‚ β”‚  Canvas  β”‚ β”‚  Users   β”‚    β”‚  β”‚
β”‚  β”‚  β”‚  Routes  β”‚ β”‚  Routes  β”‚ β”‚  Routes  β”‚ β”‚  Routes  β”‚    β”‚  β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                               β”‚                                    β”‚
β”‚                               β–Ό                                    β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚                   Service Layer                             β”‚  β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚  β”‚
β”‚  β”‚  β”‚   Auth   β”‚ β”‚   Room   β”‚ β”‚  Canvas  β”‚ β”‚   Sync   β”‚    β”‚  β”‚
β”‚  β”‚  β”‚ Service  β”‚ β”‚ Service  β”‚ β”‚ Service  β”‚ β”‚ Service  β”‚    β”‚  β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                               β”‚                                    β”‚
β”‚                               β–Ό                                    β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚                   Repository Layer                          β”‚  β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚  β”‚
β”‚  β”‚  β”‚ Postgres β”‚ β”‚  Redis   β”‚ β”‚  GORM    β”‚ β”‚  WebSocketβ”‚   β”‚  β”‚
β”‚  β”‚  β”‚  Repos   β”‚ β”‚  Repos   β”‚ β”‚  ORM     β”‚ β”‚  Hub     β”‚   β”‚  β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚
                               β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                      Data Storage Layer                            β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚       PostgreSQL            β”‚ β”‚           Redis             β”‚  β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚ β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚  β”‚
β”‚  β”‚  β”‚  Users              β”‚    β”‚ β”‚  β”‚  Sessions           β”‚    β”‚  β”‚
β”‚  β”‚  β”‚  Rooms              β”‚    β”‚ β”‚  β”‚  Pub/Sub Channels   β”‚    β”‚  β”‚
β”‚  β”‚  β”‚  Canvas Objects     β”‚    β”‚ β”‚  β”‚  Room State Cache   β”‚    β”‚  β”‚
β”‚  β”‚  β”‚  Sync Events        β”‚    β”‚ β”‚  β”‚  User Presence      β”‚    β”‚  β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚ β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Frontend Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     Frontend Architecture                          β”‚
β”‚                                                                     β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚                    Presentation Layer                       β”‚  β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”‚  β”‚
β”‚  β”‚  β”‚    Pages     β”‚ β”‚  Components  β”‚ β”‚   Layouts    β”‚      β”‚  β”‚
β”‚  β”‚  β”‚  (App Dir)   β”‚ β”‚  (Reusable)  β”‚ β”‚  (Structure) β”‚      β”‚  β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                               β”‚                                    β”‚
β”‚                               β–Ό                                    β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚                     Logic Layer                             β”‚  β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”‚  β”‚
β”‚  β”‚  β”‚    Hooks     β”‚ β”‚   Stores     β”‚ β”‚   Services   β”‚      β”‚  β”‚
β”‚  β”‚  β”‚   (Custom)   β”‚ β”‚   (Zustand)  β”‚ β”‚   (Libs)     β”‚      β”‚  β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                               β”‚                                    β”‚
β”‚                               β–Ό                                    β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚                    Integration Layer                        β”‚  β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”‚  β”‚
β”‚  β”‚  β”‚  Fabric.js   β”‚ β”‚  Matter.js   β”‚ β”‚   Yjs        β”‚      β”‚  β”‚
β”‚  β”‚  β”‚  (Canvas)    β”‚ β”‚  (Physics)   β”‚ β”‚   (CRDT)     β”‚      β”‚  β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚  β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”‚  β”‚
β”‚  β”‚  β”‚  IndexedDB   β”‚ β”‚  WebSocket   β”‚ β”‚   REST API   β”‚      β”‚  β”‚
β”‚  β”‚  β”‚  (Offline)   β”‚ β”‚  (Live)      β”‚ β”‚   (HTTP)     β”‚      β”‚  β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Data Flow

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                         Data Flow Diagram                          β”‚
β”‚                                                                     β”‚
β”‚  User Action ──► Component ──► Hook/Store ──► Service/API          β”‚
β”‚      β”‚                                       β”‚                      β”‚
β”‚      β”‚                                       β–Ό                      β”‚
β”‚      β”‚                              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”             β”‚
β”‚      β”‚                              β”‚  HTTP Request  β”‚             β”‚
β”‚      β”‚                              β”‚  WebSocket     β”‚             β”‚
β”‚      β”‚                              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜             β”‚
β”‚      β”‚                                       β”‚                      β”‚
β”‚      β”‚                                       β–Ό                      β”‚
β”‚      β”‚                              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”             β”‚
β”‚      β”‚                              β”‚   Backend      β”‚             β”‚
β”‚      β”‚                              β”‚   Processing   β”‚             β”‚
β”‚      β”‚                              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜             β”‚
β”‚      β”‚                                       β”‚                      β”‚
β”‚      β”‚                                       β–Ό                      β”‚
β”‚      β”‚                              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”             β”‚
β”‚      β”‚                              β”‚   Broadcast   β”‚             β”‚
β”‚      β”‚                              β”‚   to Others   β”‚             β”‚
β”‚      β”‚                              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜             β”‚
β”‚      β”‚                                       β”‚                      β”‚
β”‚      β”‚                                       β–Ό                      β”‚
β”‚      └───────────────────────────────► β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”‚
β”‚                                         β”‚   Update UI    β”‚          β”‚
β”‚                                         β”‚   (Realtime)   β”‚          β”‚
β”‚                                         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ› οΈ Technology Stack

Frontend

Technology Purpose
Next.js 16 (App Router, Turbopack) React framework
React 19 UI library
TypeScript Type safety
Tailwind CSS v4 Styling and design system
Fabric.js v6 Canvas rendering and object management
Matter.js Physics simulation engine
Yjs CRDT-based real-time collaboration
Zustand State management
WebSocket Real-time communication
IndexedDB (via localforage) Offline operation queue
ESLint (eslint-config-next) Linting

Backend

Technology Purpose
Go 1.25+ Backend language
Gin Web framework
PostgreSQL 15+ Primary database
Redis 7+ Session management, pub/sub & rate limiting
GORM ORM library
Gorilla WebSocket WebSocket implementation
golang-migrate Database migrations
go-redis Redis client
AWS SDK v2 (S3) Optional media storage backend (falls back to local disk)
JWT Authentication
golangci-lint Linting

πŸ“ Project Structure

Repository Root

real-time-canvas/
β”œβ”€β”€ .github/workflows/           # CI: real-time-canvas-web-ci.yml, real-time-canvas-service-ci.yml
β”œβ”€β”€ docs/                        # Diagrams and other project documentation
β”‚   └── architecture.png         # System architecture diagram (embedded above)
β”œβ”€β”€ real-time-canvas-web/        # Next.js frontend β€” see its own README
β”œβ”€β”€ real-time-canvas-service/    # Go backend β€” see its own README
└── README.md                    # This file

Frontend Structure

real-time-canvas-web/
β”œβ”€β”€ app/                         # Next.js App Router
β”‚   β”œβ”€β”€ (auth)/login/           # Login page
β”‚   β”œβ”€β”€ (canvas)/               # Canvas workspace
β”‚   β”‚   β”œβ”€β”€ page.tsx            # Room launcher dashboard
β”‚   β”‚   └── room/[roomId]/      # Individual canvas room
β”‚   β”œβ”€β”€ api/                    # Route handlers (NextAuth, room proxying)
β”‚   β”œβ”€β”€ providers/              # Context providers
β”‚   └── layout.tsx              # Root layout
β”œβ”€β”€ components/
β”‚   β”œβ”€β”€ canvas/
β”‚   β”‚   β”œβ”€β”€ InfiniteCanvas/     # Core canvas (Core/Events/Renderer split)
β”‚   β”‚   β”œβ”€β”€ tools/              # Toolbar + Text/Shape/Image/StickyNote/Pencil/Audio tools
β”‚   β”‚   β”œβ”€β”€ objects/            # Fabric object wrappers (Shape/Text/Sticky/Image/Audio)
β”‚   β”‚   β”œβ”€β”€ dock/                # Dockable tool rail + color palette
β”‚   β”‚   β”œβ”€β”€ minimap/             # Radar minimap
β”‚   β”‚   β”œβ”€β”€ physics/             # Physics engine + Matter.js bridge + controls
β”‚   β”‚   β”œβ”€β”€ ZoomControls.tsx
β”‚   β”‚   └── TimeTravelControls.tsx
β”‚   β”œβ”€β”€ collaboration/          # Presence, cursors, typing indicators
β”‚   β”œβ”€β”€ room/                   # Room list/invite/info/create/join dialogs
β”‚   β”œβ”€β”€ export/                 # PNG/SVG/JSON exporters
β”‚   └── ui/                     # Shared primitives (Button, Modal, Toast, ...)
β”œβ”€β”€ hooks/                      # useCanvas, useWebSocket, usePhysics, useOfflineSync,
β”‚                                # useTimeTravel, useRoom, useAuth, useCollaboration,
β”‚                                # useMinimap, useZoomPan
β”œβ”€β”€ lib/
β”‚   β”œβ”€β”€ canvas/                 # Fabric config, renderer, object factory, minimap renderer
β”‚   β”œβ”€β”€ websocket/               # WebSocket client, message handlers, event types
β”‚   β”œβ”€β”€ offline/                 # IndexedDB, queue manager, sync engine
β”‚   β”œβ”€β”€ time-travel/             # Event store + replay engine
β”‚   β”œβ”€β”€ physics/                 # Matter.js physics engine
β”‚   β”œβ”€β”€ yjs/                     # CRDT collaboration (crdt/sync/provider)
β”‚   β”œβ”€β”€ theme/                   # Theme provider (light/dark)
β”‚   β”œβ”€β”€ api/                     # REST client
β”‚   └── utils/                   # coordinates, debounce, uuid
β”œβ”€β”€ store/                      # Zustand stores: canvas, collaboration, drawing, export,
β”‚                                # history, minimap, room, auth, user, websocket
β”œβ”€β”€ types/                      # TypeScript type definitions
β”œβ”€β”€ config/                     # Constants & environment config
β”œβ”€β”€ public/                     # Static assets
β”œβ”€β”€ eslint.config.mjs
β”œβ”€β”€ next.config.ts
β”œβ”€β”€ package.json
β”œβ”€β”€ tailwind.config.ts
β”œβ”€β”€ tsconfig.json
└── README.md

Backend Structure

real-time-canvas-service/
β”œβ”€β”€ api/
β”‚   └── routes.go                   # HTTP route definitions
β”œβ”€β”€ cmd/api/
β”‚   └── main.go                     # Application entry point (wiring + startup)
β”œβ”€β”€ internal/
β”‚   β”œβ”€β”€ config/                     # Config loading, DB connections, migration runner
β”‚   β”œβ”€β”€ handlers/                   # auth, room, canvas, sync, media, websocket
β”‚   β”œβ”€β”€ middleware/                 # auth (JWT), cors, logging, ratelimit (Redis-backed)
β”‚   β”œβ”€β”€ models/                     # user, room, room_user, canvas_object, sync_event
β”‚   β”‚   └── dto/                    # Request/response DTOs
β”‚   β”œβ”€β”€ repository/
β”‚   β”‚   β”œβ”€β”€ postgres/               # user, room, canvas, sync repositories (GORM)
β”‚   β”‚   └── redis/                  # session repository, pub/sub
β”‚   β”œβ”€β”€ services/                   # user, room, canvas, sync, media, physics
β”‚   β”œβ”€β”€ storage/                    # Pluggable media storage: local disk or S3
β”‚   └── websocket/                  # hub, client, message types (+ hub concurrency tests)
β”œβ”€β”€ migrations/                     # golang-migrate SQL migrations
β”œβ”€β”€ pkg/
β”‚   β”œβ”€β”€ database/                   # Postgres/Redis connection helpers
β”‚   β”œβ”€β”€ jwt/                        # Token generation/validation (+ tests)
β”‚   β”œβ”€β”€ redis/                      # Shared Redis service (rate limiting, pub/sub)
β”‚   └── utils/                      # id_generator, validator
β”œβ”€β”€ uploads/                        # Local media storage fallback (gitignored)
β”œβ”€β”€ go.mod
β”œβ”€β”€ go.sum
β”œβ”€β”€ .env.example
└── README.md

πŸš€ Getting Started

Prerequisites

Frontend:

  • Node.js 22+
  • npm (the repo ships a package-lock.json, so use npm ci/npm install)

Backend:

  • Go 1.25+
  • PostgreSQL 15+
  • Redis 7+

Installation

# Clone the repository
git clone https://github.com/Oluwaseyi89/real-time-canvas.git
cd real-time-canvas

# ─── Frontend Setup ───
cd real-time-canvas-web
npm install

cp .env.example .env

# ─── Backend Setup ───
cd ../real-time-canvas-service
go mod download
cp .env.example .env

# Edit .env with your PostgreSQL and Redis credentials
nano .env

Environment Variables

See each app's own README for the full, current list β€” real-time-canvas-web/.env.example and real-time-canvas-service/.env.example are the source of truth. At minimum:

Frontend (.env):

NEXT_PUBLIC_APP_NAME=Infinite Canvas
NEXT_PUBLIC_API_URL=http://localhost:8080/api/v1
NEXT_PUBLIC_WS_URL=ws://localhost:8080/ws

Backend (.env):

ENVIRONMENT=development
PORT=8080

# PostgreSQL
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=postgres
DB_NAME=collaborative_canvas
DB_SSLMODE=disable

# Redis
REDIS_URL=redis://localhost:6379

# CORS β€” comma-separated origins allowed to make credentialed requests
ALLOWED_ORIGINS=http://localhost:3000

# Security
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production

# Media uploads β€” leave S3_BUCKET empty to use local disk storage instead
S3_BUCKET=
LOCAL_UPLOAD_DIR=./uploads
LOCAL_UPLOAD_BASE_URL=http://localhost:8080/uploads
MEDIA_MAX_UPLOAD_MB=15

Running the Application

# ─── Backend ───
cd real-time-canvas-service

# Applies pending migrations on startup, then serves on :8080
go run cmd/api/main.go

# ─── Frontend ───
cd ../real-time-canvas-web

# Development mode
npm run dev

# Production build
npm run build
npm start

πŸ”Œ API Endpoints

Authentication

Method Endpoint Description
POST /api/v1/auth/register Register new user
POST /api/v1/auth/login Login user
POST /api/v1/auth/guest Guest login
GET /api/v1/auth/profile Get user profile

Rooms

Method Endpoint Description
POST /api/v1/rooms Create room
GET /api/v1/rooms Get user rooms
GET /api/v1/rooms/:id Get room by ID
PUT /api/v1/rooms/:id Update room
DELETE /api/v1/rooms/:id Delete room
POST /api/v1/rooms/:id/join Join room
POST /api/v1/rooms/:id/leave Leave room
GET /api/v1/rooms/:id/users Get room users

Canvas Objects

Method Endpoint Description
POST /api/v1/rooms/:id/objects Create object
GET /api/v1/rooms/:id/objects Get room objects
GET /api/v1/rooms/:id/objects/:objId Get object by ID
PUT /api/v1/rooms/:id/objects/:objId Update object
DELETE /api/v1/rooms/:id/objects/:objId Delete object
POST /api/v1/rooms/:id/objects/batch Batch create objects
POST /api/v1/rooms/:id/objects/clear Clear room objects

Sync & Media

Method Endpoint Description
POST /api/v1/rooms/:id/events Record a sync event (time-travel/offline replay log)
GET /api/v1/rooms/:id/events Fetch missed events since a version, for reconnect/replay
POST /api/v1/rooms/:id/media Upload an image/audio attachment (local disk or S3)

WebSocket

Endpoint Description
/ws WebSocket connection endpoint (auth via ?token= query param)

All routes above /api/v1/auth/* require a Bearer JWT. Auth endpoints are rate-limited per IP (20 req/min); all other /api/v1 routes are rate-limited per authenticated user (300 req/min), backed by Redis.

πŸ” WebSocket Events

Client β†’ Server

  • room:join – Join a room
  • room:leave – Leave a room
  • user:presence – Update presence
  • user:cursor – Update cursor position
  • user:typing – Update typing status
  • object:create – Create object
  • object:update – Update object
  • object:delete – Delete object
  • physics:* – Physics events (throw, collision, attract, repel)

Server β†’ Client

  • connection:ack – Connection acknowledgment
  • room:joined – Room joined successfully
  • room:left – Room left successfully
  • user:joined – User joined room
  • user:left – User left room
  • user:presence – User presence update
  • user:cursor – User cursor update
  • user:typing – User typing status
  • object:create – Object created
  • object:update – Object updated
  • object:delete – Object deleted
  • canvas:sync – Canvas synchronization
  • physics:* – Physics events

🎨 Design System

Theme Colors

  • Background: #090d16 (Dark space)
  • Canvas Background: #0b0f19
  • Primary: #6366f1 (Indigo)
  • Accent: #06b6d4 (Cyan)
  • Surface: rgba(15, 23, 42, 0.75) (Glassmorphism)

Glassmorphism

All panels use a consistent glassmorphism style with:

  • backdrop-blur effects
  • Subtle border opacity
  • Shadow layers
  • Smooth transitions

Typography

  • Primary font: System UI (SF Pro, Segoe UI)
  • Mono font: For technical elements and code
  • Font sizes: Scale from 10px to 48px

πŸ“ˆ Performance Considerations

Frontend

  • Object Rendering: Fabric.js handles 100+ objects efficiently with viewport culling
  • Physics: Matter.js runs at 60fps with configurable gravity and time scale
  • WebSocket: Binary message formats for efficient data transfer
  • Offline: IndexedDB for operation queue with batched sync

Backend

  • Database Indexes: Optimized queries with proper indexing
  • Redis Caching: Session and room state caching for fast retrieval
  • Connection Pooling: Efficient database and Redis connection management
  • Horizontal Scaling: WebSocket hubs can be scaled with Redis pub/sub

βœ… Continuous Integration

Each app has its own workflow under .github/workflows/, scoped to only run when that app's files change:

Run the same checks locally before pushing β€” see the Quality & CI section in each app's README for exact commands.

🀝 Contributing

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Run the relevant app's lint/type-check/build/test commands locally (see its README)
  4. Commit your changes (git commit -m 'Add amazing feature')
  5. Push to the branch (git push origin feature/amazing-feature)
  6. Open a Pull Request β€” CI will run automatically against it

πŸ“„ License

No LICENSE file is currently published for this repository, so default copyright applies (all rights reserved) unless/until the maintainer adds one.

About

Real Time Canvas is a Hackathon task at Vega IT to build a web platform that allows multiple users to work together on an infinite canvas in real time

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages