How SimStudio AI / Sim Implements Real-Time Collaborative Canvas with Socket.IO
SimStudio AI / Sim powers its workflow editor's real-time collaboration through a dedicated Socket.IO server that validates, persists, and broadcasts workflow operations across users, scaling from single-pod to multi-pod deployments via Redis.
The simstudioai/sim repository implements a robust real-time collaborative canvas for its visual workflow editor using a dedicated Socket.IO server located in apps/realtime. This architecture enables multiple users to simultaneously edit workflows with immediate synchronization of changes, role-based permissions, and automatic persistence to the database.
Server Bootstrap and Configuration
The collaboration server initializes in apps/realtime/src/index.ts by creating an HTTP server and attaching a configured Socket.IO instance.
const httpServer = createServer()
const io = await createSocketIOServer(httpServer) // ← config in config/socket.ts
io.use(authenticateSocket) // ← auth middleware
io.on('connection', (socket) => setupAllHandlers(socket, roomManager))
The createSocketIOServer function in apps/realtime/src/config/socket.ts handles CORS configuration, ping intervals, and critical scaling logic. When the REDIS_URL environment variable is present, the server creates a Redis adapter using @socket.io/redis-adapter to enable multi-pod broadcasting.
const io = new Server(httpServer, { cors: …, pingTimeout: … })
if (env.REDIS_URL) {
const pub = createClient(redisOptions)
const sub = createClient(redisOptions)
await Promise.all([pub.connect(), sub.connect()])
io.adapter(createAdapter(pub, sub))
}
This configuration allows the system to run in single-pod mode (using in-memory MemoryRoomManager) or multi-pod mode (using RedisRoomManager) without code changes, driven entirely by the presence of Redis configuration.
Authentication and Room Management
Before accessing collaborative features, every socket must pass through the authentication middleware in apps/realtime/src/middleware/auth.ts. This validates the session cookie using the shared Better-Auth secret and decorates the socket with a typed AuthenticatedSocket containing userId, userName, and other user metadata.
Room management is abstracted through apps/realtime/src/rooms/memory-manager.ts and apps/realtime/src/rooms/redis-manager.ts. These managers maintain mappings between sockets and workflow rooms, track per-user presence (including roles and last activity), and expose helper methods such as:
getWorkflowIdForSocketgetUserSessionhasWorkflowRoomupdateUserActivity
Operation Handling and Broadcasting
The core collaboration logic resides in apps/realtime/src/handlers/operations.ts. When a client emits a workflow-operation event, the handler executes a strict validation and authorization pipeline:
- Validation: The payload is validated against
WorkflowOperationSchemausing Zod - Authorization: The handler checks user role permissions via
checkRolePermissionand enforces workflow mutability withassertWorkflowMutable - Persistence vs. Broadcasting: The system distinguishes between high-frequency position updates and critical state changes
- Position updates (block moves) are broadcast first to minimize latency, with optional persistence if marked as committed
- Other operations are persisted first via
persistWorkflowOperation, then broadcast to room members
After successful persistence, the server broadcasts changes to all other sockets in the workflow room:
socket.to(workflowId).emit('workflow-operation', broadcastData)
socket.emit('operation-confirmed', { operationId, serverTimestamp: Date.now() })
On failure, the originating client receives operation-failed or operation-error with detailed debugging information.
Client-Side Integration
Clients connect using standard Socket.IO client libraries. The following React hook demonstrates connecting to the collaborative canvas, joining a workflow room, and handling remote operations:
import { io, Socket } from 'socket.io-client'
import { useEffect, useRef } from 'react'
export function useCollaborativeWorkflow(workflowId: string) {
const socketRef = useRef<Socket | null>(null)
useEffect(() => {
const socket = io(import.meta.env.VITE_SOCKET_URL, {
withCredentials: true,
transports: ['websocket'],
})
socketRef.current = socket
// Join the workflow room
socket.emit('join-workflow', { workflowId })
// Listen for remote operations
socket.on('workflow-operation', (data) => {
// Apply the operation to the local state (e.g. via Zustand or React-Query)
applyRemoteOperation(data)
})
// Cleanup on unmount
return () => {
socket.disconnect()
}
}, [workflowId])
// Send a local operation
const sendOperation = (op) => {
socketRef.current?.emit('workflow-operation', op)
}
return { sendOperation }
}
Presence and Graceful Shutdown
Additional handlers in apps/realtime/src/handlers/variables.ts and apps/realtime/src/handlers/subblocks.ts manage specialized updates for workflow variables and subblocks. The system tracks user presence and automatically cleans up pending operation IDs when sockets disconnect to prevent memory leaks.
For graceful shutdown, apps/realtime/src/config/socket.ts exports shutdownSocketIOAdapter to close Redis connections, while apps/realtime/src/index.ts implements a top-level shutdown routine:
const shutdown = async () => {
await roomManager.shutdown()
await shutdownSocketIOAdapter()
httpServer.close(() => process.exit(0))
}
process.on('SIGINT', shutdown)
process.on('SIGTERM', shutdown)
Summary
- Socket.IO server architecture: The
apps/realtimepackage runs as a dedicated service with configurable Redis adapter support for horizontal scaling - Strict validation pipeline: Every operation passes through Zod schema validation, role-based permission checks, and mutability assertions before processing
- Optimized broadcasting: Position updates broadcast immediately for responsiveness, while state-changing operations persist to database before broadcasting to ensure durability
- Room abstraction: The
RoomManagerinterface (with Memory and Redis implementations) handles socket-to-workflow mappings and user presence across single or multi-pod deployments - Type-safe authentication: Middleware enforces session validation using Better-Auth and decorates sockets with typed user metadata for secure collaboration
Frequently Asked Questions
How does SimStudio AI handle conflicts when multiple users edit simultaneously?
The system processes operations sequentially on the server. Each workflow-operation is validated against WorkflowOperationSchema and authorized via checkRolePermission before being persisted. For high-frequency updates like dragging blocks, the server broadcasts position changes immediately to all clients in the room via socket.to(workflowId).emit(), ensuring all participants see live cursor movements and block repositioning in real-time.
Can the real-time collaboration scale across multiple server instances?
Yes. When REDIS_URL is configured, apps/realtime/src/config/socket.ts initializes a Redis adapter using @socket.io/redis-adapter with separate publisher and subscriber clients. This allows Socket.IO to broadcast messages across all pods using the RedisRoomManager implementation, while single-pod deployments use the MemoryRoomManager without code changes.
What happens if a user loses connection during an edit?
The room manager tracks user sessions and activity timestamps through updateUserActivity. When a socket disconnects, cleanup handlers remove pending operation IDs and update room state. Upon reconnection, the client rejoins the workflow room via join-workflow and receives the current state, with any missed operations handled through the standard acknowledgment pattern (operation-confirmed or operation-failed).
How are permissions enforced in the collaborative canvas?
The authenticateSocket middleware in apps/realtime/src/middleware/auth.ts validates the session cookie and attaches user metadata to the socket. Before executing any operation in apps/realtime/src/handlers/operations.ts, the system calls checkRolePermission to verify the user has appropriate access rights (e.g., editor vs. viewer) and assertWorkflowMutable to ensure the workflow allows modifications. Unauthorized operations are rejected before reaching the persistence layer.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →