AstraDraw Collaboration System
This document describes the real-time collaboration system in AstraDraw, including how it integrates with workspaces, teams, collections, and user profiles.
AstraDraw supports two collaboration modes:
Mode
Description
Authentication
Use Case
Workspace Collaboration
Permission-based, tied to scenes in shared workspaces
Required
Team collaboration
Legacy Anonymous Mode
Link-based, no authentication
Not required
Quick sharing, backward compatibility
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Frontend (React) β
β β
β ββββββββββββββββββββ ββββββββββββββββββββ ββββββββββββββββββββ β
β β AuthContext βββββΊβ authUserAtom βββββΊβ Collab β β
β β (User Profile) β β (Jotai Store) β β (Manages Session)β β
β ββββββββββββββββββββ ββββββββββββββββββββ ββββββββββ¬ββββββββββ β
β β β
β ββββββββββββββββββββ ββββββββββββββββββββ β β
β β SceneAccessCheck ββββββ workspaceApi β β β
β β (Permissions) β β (API Client) β β β
β ββββββββββββββββββββ ββββββββββββββββββββ β β
β βΌ β
β ββββββββββββββββββββ β
β β Portal β β
β β (WebSocket Msgs) β β
β ββββββββββ¬ββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββ€
β β
βΌ βΌ
βββββββββββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββββββ
β Backend (NestJS) β β Room Service (Node.js) β
β β β β
β βββββββββββββββββββββββββββββββ β β βββββββββββββββββββββββββββ β
β β SceneAccessService β β β β WebSocket Relay β β
β β - Workspace permissions β β β β - Broadcasts messages β β
β β - Team β Collection access β β β β - E2E encrypted β β
β β - Collaboration eligibilityβ β β β - Stateless β β
β βββββββββββββββββββββββββββββββ β β βββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββ
β PostgreSQL + S3 β
β - Scene metadata & permissions β
β - Scene data (encrypted) β
βββββββββββββββββββββββββββββββββββββ
Part 1: Workspace Types and Permissions
Type
Description
Collaboration
Personal
Created automatically for each user
Not allowed
Shared
Created explicitly, supports teams
Allowed
User
βββΊ WorkspaceMember (role: ADMIN / MEMBER / VIEWER)
βββΊ TeamMember
βββΊ Team
βββΊ TeamCollection (accessLevel: VIEW / EDIT)
βββΊ Collection
βββΊ Scene
Workspace Type
Collection Type
Who Can Collaborate
Personal
Any
No one (collaboration disabled)
Shared
Private
Only the owner
Shared
Team collection
Team members with EDIT access
Access Level
Can View
Can Edit
Can Collaborate
No access
β
β
β
VIEW
β
β
β (view-only in room)
EDIT
β
β
β
ADMIN
β
β
β
https://example.com/workspace/{slug}/scene/{sceneId}#key={roomKey}
Component
Description
slug
Workspace URL-friendly identifier
sceneId
Scene database ID
roomKey
Encryption key (in hash, never sent to server)
Flow:
User opens URL
If not authenticated β redirect to login
Backend checks permissions via SceneAccessService
If authorized β load scene and join collaboration room
If #key present β enable real-time sync
https://example.com/#room={roomId},{roomKey}
Component
Description
roomId
Random 20-character ID
roomKey
Encryption key
Flow:
User opens URL
No authentication required
Join room directly
Full edit access for anyone with link
Part 3: Profile Integration
When users collaborate, their identity is displayed to other participants.
Authenticated users appear with their profile name and avatar
Anonymous users use randomly generated names (editable)
Profile data is transmitted via encrypted WebSocket messages
The Collab component is a class component, so it accesses user data via Jotai:
// AuthContext syncs to atom
useEffect ( ( ) => {
appJotaiStore . set ( authUserAtom , user ) ;
} , [ user ] ) ;
// Collab reads from atom
const authUser = appJotaiStore . get ( authUserAtom ) ;
if ( authUser ?. name ) {
this . setUsername ( authUser . name ) ;
}
Priority Order for Username
Authenticated user name (highest priority)
localStorage saved name (from previous sessions)
Random generated name (fallback)
WebSocket Message Payloads
Mouse Location:
{
type : "MOUSE_LOCATION" ,
payload : {
socketId : "..." ,
pointer : { x, y } ,
button : "up" | "down" ,
selectedElementIds : { ...} ,
username : "John Doe" ,
avatarUrl : "data:image/..." // Base64
}
}
Idle Status:
{
type : "IDLE_STATUS" ,
payload : {
socketId : "..." ,
userState : "active" | "idle" | "away" ,
username : "John Doe" ,
avatarUrl : "data:image/..."
}
}
All scene data and messages are encrypted client-side
Encryption key is in URL hash (never sent to server)
Room service only relays encrypted payloads
Only room participants can decrypt
Workspace scenes : Backend validates access before returning room credentials
Anonymous rooms : Anyone with link has access (by design)
Room service is stateless - permission checks happen at API level
Stored as base64 data URLs in database
Transmitted within encrypted WebSocket payloads
No external image loading (prevents tracking)
Mode
Metadata
Scene Data
Files
Workspace
PostgreSQL scenes table
S3 {storageKey}
S3 files/{id}
Anonymous
None
S3 rooms/{roomId}
S3 files/{id}
model Scene {
id String @id
title String
storageKey String @unique
// Collaboration
roomId String ? // Collaboration room ID
roomKeyEncrypted String ? // Encrypted room key (server-side)
collaborationEnabled Boolean @default (true )
// Ownership & Organization
userId String
collectionId String ?
// ...
}
When sharing a workspace scene:
βββββββββββββββββββββββββββββββββββββββββββ
β Share this scene β
β β
β Team members with access to this β
β collection can collaborate in β
β real-time. β
β β
β [Enable collaboration] β
β β
β βββββββββββ or βββββββββββ β
β β
β [Start anonymous board] β
β (Creates a separate, unlinked board) β
βββββββββββββββββββββββββββββββββββββββββββ
Top-right corner shows connected collaborators:
With avatar : Profile picture in circle
Without avatar : Colored circle with initials
Hover : Shows username tooltip
Part 7: Implementation Status
Backend Completed (December 21, 2025)
File
Purpose
excalidraw-app/app-jotai.ts
authUserAtom definition
excalidraw-app/auth/AuthContext.tsx
Syncs user to atom
excalidraw-app/collab/Collab.tsx
Collaboration session management
excalidraw-app/collab/Portal.tsx
WebSocket message handling
excalidraw-app/data/index.ts
Link generation, message types
excalidraw-app/data/httpStorage.ts
Room data storage
excalidraw-app/share/ShareDialog.tsx
Share UI
File
Purpose
src/workspace/workspace-scenes.controller.ts
Scene API endpoints
src/workspace/scene-access.service.ts
Permission checking (planned)
prisma/schema.prisma
Database schema
File
Purpose
src/index.ts
WebSocket relay server
Permission-Based Collaboration (After Implementation)
Part 8: Comment Real-time Sync
Comments are synchronized in real-time during collaboration sessions using a dedicated WebSocket event channel.
User A creates comment
β
useCommentMutations calls API (creates in database)
β
onSuccess: emitEvent({ type: "thread-created", thread })
β
socket.emit("comment:event", roomId, event)
β
room-service: socket.broadcast.to(roomId).emit("comment:event", event)
β
User B's useCommentSync receives event
β
React Query cache updated β UI updates instantly
Event Type
Payload
Description
thread-created
{ thread: CommentThread }
New thread created
thread-resolved
{ threadId, resolved }
Thread resolved/reopened
thread-deleted
{ threadId }
Thread deleted
thread-moved
{ threadId, x, y }
Thread position changed
comment-added
{ threadId, comment }
Reply added to thread
comment-updated
{ commentId, content }
Comment edited
comment-deleted
{ threadId, commentId }
Comment deleted
File
Purpose
frontend/excalidraw-app/hooks/useCommentSync.ts
Listens for events, updates cache
frontend/excalidraw-app/components/Comments/CommentSyncContext.tsx
Provides emitEvent to components
frontend/excalidraw-app/hooks/useCommentThreads.ts
Emits events after mutations
room-service/src/index.ts
Relays comment:event to room
Plain JSON (not encrypted) : Unlike drawing data, comments are stored server-side in PostgreSQL, so encryption is not needed for WebSocket relay
Optimistic + Server : UI updates optimistically, then syncs via WebSocket; duplicates are filtered by ID
Context-based injection : CommentSyncProvider provides emitEvent to all comment components without prop drilling
Graceful degradation : If not collaborating, emitEvent is a no-op; comments still work via API
Avatar Size : Base64 avatars transmitted with every position update
Profile Updates : Changes during session require reconnection
Room Key Storage : Currently encrypted server-side; consider client-side key derivation for maximum privacy
Date
Changes
2025-12-24
Added Part 8: Comment Real-time Sync documentation
2025-12-21
Initial collaboration profile integration
2025-12-21
Documented permission model and workspace types
2025-12-21
Created implementation plan for permission-based collaboration