1. Syncode
Syncode
  • Syncode
    • Conventions
    • Response Schemas
    • Error Taxonomy
    • Resource Model
    • Permission Model
    • Cross-Cutting Concerns
    • Security
    • Collab-Plane
    • Execution-Plane
    • AI-Plane
  • SynCode Control Plane API
    • Auth
      • Create a new account
      • Authenticate and get tokens
      • Refresh access token
      • Invalidate refresh token
      • Change current user's password
      • Request password reset email
      • Reset password with token
    • Users
      • Get current user profile
      • Update current user profile
      • Soft-delete account
      • Get public profile of another user
      • Upload avatar (presigned URL)
      • Get usage quotas and limits
      • Get current active room (for reconnection)
      • Get time-series training statistics
    • Rooms
      • Participants
        • List all participants in a room
        • Update participant (role, mute)
        • Kick a participant from the room
      • Control
        • Advance room phase
        • Select or change the problem
        • Update room settings
        • Lock code editor, run, and submit
        • Unlock code editor, run, and submit
        • Pause the coding timer
        • Resume the coding timer
        • Request a role swap (peer mode)
        • Accept or decline a role swap request
      • Media
        • Generate LiveKit access token
        • Record participant's recording consent
        • Start session recording
        • Stop session recording
      • AI
        • Send a message to AI interviewer
        • Poll AI message result
        • Get AI conversation history
        • Request a targeted hint
        • Get hint result
        • Request code review
        • Get review result
        • Get cross-session weakness tracking
      • StaticAnalysis
        • Request static analysis
        • Get analysis result
      • Feedback
        • Submit peer evaluation
        • Get all feedback for this room
        • Get my submitted feedback
      • Create a new room
      • List rooms for current user
      • Get room details
      • Destroy a room (host only)
      • Join a room via room code
      • Leave a room
      • Lookup room by invite code
      • Execute code (interactive run)
      • Submit code against test cases
      • List past runs in this room
      • List past submissions in this room
    • Problems
      • List and search problems
      • Create a problem (admin)
      • List all available tags
      • Get problem details
      • Update a problem (admin)
      • Delete a problem (admin)
    • Bookmarks
      • List bookmarked problems
      • Bookmark a problem
      • Remove bookmark
    • Execution
      • Get execution result (single run)
      • Get submission status and aggregated results
    • Sessions
      • List my session history
      • Get session details
      • Soft-delete a session
      • Get training report
      • Get session event timeline
      • Get code snapshots
      • Get recording download URL
      • Get peer feedback for this session
      • Get whiteboard export
      • Get AI conversation history
      • Compare multiple session reports
    • Matchmaking
      • Enter the matchmaking queue
      • Cancel matchmaking
      • Get current match status
      • Accept a proposed match
      • Decline a proposed match
    • Admin
      • System overview stats
      • List all users
      • Get user details (admin view)
      • Update user (ban, role change)
      • List all rooms
      • Force-close a room
      • Query audit logs
    • Health
      • Deep health check
    • Schemas
      • RoomStatus
      • CreateDocumentRequest
      • RoomRole
      • CreateDocumentResponse
      • RoomMode
      • DestroyDocumentResponse
      • SupportedLanguage
      • KickUserRequest
      • Difficulty
      • KickUserResponse
      • UserRole
      • LockEditorRequest
      • ErrorResponse
      • LockEditorResponse
      • Pagination
      • SnapshotReadyPayload
      • UserProfile
      • UserDisconnectedPayload
      • PublicProfile
      • CallbackAckResponse
      • RoomConfig
      • RoomParticipantSummary
      • RoomSummary
      • RoomDetail
      • RoomPreview
      • ProblemSummary
      • ProblemDetail
      • ProblemExample
      • TestCase
      • TagInfo
      • AiMessage
      • WeaknessEntry
      • PeerFeedbackRatings
      • PeerFeedbackEntry
      • SessionSummary
      • SessionDetail
      • SessionParticipant
      • SessionEvent
      • CodeSnapshot
      • Evidence
      • ReportDimension
      • AdminDashboard
      • AdminUserEntry
      • AdminUserDetail
      • AdminRoomEntry
      • AuditLogEntry
      • HealthResponse
      • MatchOpponent
  • SynCode Collab Plane API
    • Documents
      • Create a Yjs document
      • Destroy a Yjs document
      • Kick a user from the document
      • Toggle editor lock
    • Health
      • Health check
    • Callbacks
      • [Callback] Snapshot ready
      • [Callback] User disconnected
    • Schemas
      • CreateDocumentRequest
      • CreateDocumentResponse
      • DestroyDocumentResponse
      • KickUserRequest
      • KickUserResponse
      • SnapshotReadyPayload
      • LockEditorRequest
      • UserDisconnectedPayload
      • LockEditorResponse
      • CallbackAckResponse
      • ErrorResponse
  1. Syncode

Collab-Plane

WebSocket server for Yjs document sync, whiteboard collaboration, and code snapshots.

Overview#

NestJS WebSocket server on port 3001. Holds Yjs CRDT documents in memory (one for code, one for whiteboard) and syncs them to connected clients. Also handles awareness (cursors, selections), editor lock enforcement, and periodic code snapshots.
Frontends connect over WebSocket with a short-lived collab token. The control-plane drives document lifecycle (create/destroy) and permission changes (lock/kick) through an internal HTTP API. When something interesting happens, such as a snapshot being ready or a user dropping off, the collab-plane POSTs back to the control-plane.

Architecture#

Communication patterns:
DirectionProtocolPurpose
Frontend --> Collab-planeWebSocketYjs sync, awareness, room events
Control-plane --> Collab-planeHTTP (internal)Document lifecycle, kick, health
Collab-plane --> Control-planeHTTP (callback)Snapshot ready, user disconnected

WebSocket Protocol#

Connection Lifecycle#

Authentication#

The control-plane issues a collab token (JWT) when a user joins a room via POST /rooms/:id/join. Payload:
Passed as a query parameter: ws://collab-plane:3001?token=<collabToken>. The POST /rooms/:roomId/join response includes the full collabUrl. Clients should use it as-is.
Validation:
Signature checked against JWT_SECRET
roomId in the token must match the room the client joins
Expired or revoked tokens get close code 4001

Message Framing#

Two message types on the wire:
TypeFormatPurpose
BinaryRaw Uint8ArrayYjs sync protocol (document updates, state vectors)
TextJSON stringControl messages (join, awareness, room events, errors)
JSON messages use this envelope:

Yjs Document Sync Protocol#

Implements the standard y-protocols sync protocol. All sync messages are binary-encoded.
y-protocols message type constants:
messageYjsSyncStep1 = 0: contains state vector
messageYjsSyncStep2 = 1: contains encoded state updates (diff)
messageYjsUpdate = 2: incremental document update
Binary encoding: every sync message is wrapped in a messageSync outer envelope (byte 0x00). The second byte picks the sub-type. Wire format: [messageType, syncSubType, ...payload].
Outer TypeInner Sub-TypeWire PrefixNameDirectionPayloadPurpose
messageSync (0x00)0x00[0, 0, ...]SyncStep1BothState vector (Uint8Array)"Here is what I have; tell me what I'm missing"
messageSync (0x00)0x01[0, 1, ...]SyncStep2BothEncoded updates (Uint8Array)"Here are the updates you're missing"
messageSync (0x00)0x02[0, 2, ...]UpdateBothIncremental update (Uint8Array)Real-time document mutation
messageAwareness (0x01)--[1, ...]AwarenessBothAwareness JSONCursor, selection, presence state
The top-level discriminators (messageSync = 0, messageAwareness = 1) are defined by y-websocket, the reference WebSocket provider. The sync sub-types (SyncStep1 = 0, SyncStep2 = 1, Update = 2) are defined by y-protocols/sync.js. y-websocket also defines messageAuth = 2 and messageQueryAwareness = 3, though these are not used here.
Standard y-protocols allows both sides to send SyncStep1 simultaneously. This implementation uses a server-first sequence instead.
Each room has two separate Yjs documents:
DocumentY.js Shared TypeContent
Code editorY.TextSource code with language metadata
WhiteboardYKeyValue<TLRecord> (backed by Y.Array)Flat store of all tldraw records (shapes, bindings, assets, pages)

Awareness Protocol#

Awareness is ephemeral, non-persisted state: cursors, selections, typing indicators. JSON-encoded and broadcast to all peers in the room.
Awareness messages:
DirectionTypePurpose
Client --> ServerawarenessClient sends own awareness state
Server --> ClientsawarenessServer broadcasts all peers' awareness states
Server --> Clientsawareness-removePeer disconnected, remove their cursor/selection
Updates are throttled to 50ms minimum interval per client. States expire after 30 seconds of inactivity (standard y-protocols behavior). The server cleans up stale entries and broadcasts removals.

Room Events#

Broadcast to all connected clients in a room:

Whiteboard (Phase 4)#

Covers drawing algorithm diagrams, collaborative annotation, and whiteboard export.
Separate Yjs document from the code editor, so the two never conflict. The frontend uses tldraw for the whiteboard UI. tldraw does not integrate with Yjs natively; it uses its own TLStore internally. A custom sync bridge (a useYjsStore hook) connects tldraw's store to the shared Yjs document, using YKeyValue from y-utility to store TLRecord objects flat in a Y.Array.

Data Model#

The whiteboard Yjs document stores all tldraw records (shapes, bindings, assets, pages) in a single flat YKeyValue map:
The sync bridge listens in both directions:
Local to remote: store.listen(callback, { source: 'user', scope: 'document' }) fires on local edits, writes changes into the Yjs doc via yDoc.transact().
Remote to local: yStore.on('change', ...) fires on incoming Yjs updates, applies them via store.mergeRemoteChanges() (which prevents echo loops).
No whiteboard-specific WebSocket messages are needed. The Yjs CRDT handles merge and broadcast through the same sync protocol used for code editing.

Drawing Capabilities#

Interviewer and candidate can draw simultaneously: freehand lines, arrows, rectangles, ellipses, text labels, sticky notes, connected flowcharts. Each user gets a color. Spectators see everything in real time but can't edit.

Export#

During a session, users can export via tldraw's Editor API:
FormatMethodNotes
PNGeditor.toImage(shapes, { format: 'png' })Returns { blob: Blob }. Use pixelRatio: 2 for high-DPI.
SVGeditor.toImage(shapes, { format: 'svg' })Vector output, suitable for print
JSONSerialize Yjs doc to JSONFull fidelity, can be re-imported
When the room transitions to finished, the collab-plane renders the whiteboard to PNG and uploads it to S3. The control-plane stores the S3 key in the session record. That is the canonical whiteboard artifact. Client-side exports during the session are for personal use only.

Code Snapshots#

Covers code diffs, pre-submission read-only snapshots, session replay timeline, and contextual replay.
Snapshots capture the Yjs document state at regular intervals, stored as binary state vectors. The control-plane uses them for code diffs, pre-submission review, and session replay.

Snapshot Triggers#

TriggerWhenInitiated By
PeriodicEvery 30 seconds during coding / wrapup phasesCollab-plane scheduler
Phase changeOn every room phase transitionControl-plane (via phase-change event)
Manual submitBefore code submission for evaluationControl-plane (locks the editor via POST /internal/documents/:roomId/lock, triggering a snapshot)
Session endWhen room transitions to finishedControl-plane (destroyDocument)

Snapshot Flow#

Snapshot Storage#

Delivered to the control-plane via the CONTROL_INTERNAL.SNAPSHOT_READY callback as a JSON array of bytes, then persisted in the code_snapshots table:
FieldTypeDescription
idUUIDPrimary key
roomIdUUIDFK to rooms
snapshotbyteaBinary Yjs state vector
timestamptimestamptzWhen the snapshot was taken
triggerenumperiodic, phase-change, submit, session-end

Diff Computation#

Diffs are computed client-side. The frontend applies two snapshots to temporary Yjs documents and compares the Y.Text contents. The collab-plane just delivers raw snapshots.
Snapshot A (t=60s) + Snapshot B (t=120s) --> frontend diff view

Read-Only Snapshot#

Before submission, the frontend fetches the latest snapshot and shows it in a read-only editor for confirmation. Only after the user approves does the control-plane forward the code to the execution-plane.

Editor Lock#

Covers locking and unlocking code editing permissions.
Enforced at both server and client. When locked, candidates can't edit; their Yjs updates are rejected server-side and the editor UI disables input.

Lock Flow#

1.
Host or interviewer toggles lock via POST /rooms/:roomId/control/lock-editor
2.
Control-plane sends lock state to the collab-plane via POST /internal/documents/:roomId/lock (see Internal HTTP API)
3.
Collab-plane sets lock state on the Yjs document metadata
4.
Broadcasts editor-lock event to all connected clients
5.
Server rejects incoming Yjs updates from locked users (not applied, not broadcast)
6.
Client disables editor input

Lock Rules#

RoleWhen LockedWhen Unlocked
HostCan editCan edit
InterviewerCan editCan edit
CandidateRead-only (updates rejected)Can edit
SpectatorAlways read-onlyAlways read-only
Whiteboard is not affected by editor lock. Both participants can always annotate regardless of code lock state.
The control-plane separately blocks POST /rooms/:roomId/run and POST /rooms/:roomId/submit when locked (returns ROOM_EDITOR_LOCKED 409). The collab-plane enforces the CRDT write lock; the control-plane enforces the execution lock.

Reconnection#

Covers reconnection and state restoration after network interruption.
Yjs CRDTs handle reconnection naturally: the sync protocol merges divergent states. The server holds the authoritative document, so reconnecting clients just re-sync.

Reconnection Flow#

Offline Edits#

Edits made while disconnected stay in the local Yjs document. On reconnect, the sync protocol exchanges missing updates in both directions and the CRDT merge converges without conflicts.

Reconnection Guarantees#

ScenarioBehavior
Brief network blip (< collab token lifetime)Auto-reconnect, full state sync, no data loss
Extended disconnection (collab token expired)Client must re-join via control-plane to get a fresh collab token
Server restartDocuments lost from memory. On restart, server pulls the latest snapshot from the control-plane to reconstruct; clients re-sync on reconnect. If no snapshot exists, clients provide full state via SyncStep1/SyncStep2.
All clients disconnectDocument stays in memory for a configurable TTL (DOCUMENT_IDLE_TTL_MS, default 5 min). Final snapshot sent to control-plane on cleanup.

Internal HTTP API#

Only the control-plane calls these. Not exposed to frontends. Secured with the X-Internal-Secret header.

Document Lifecycle#

Callback Sequence#

Error Handling for Callbacks#

If the control-plane is unreachable: retry with exponential backoff (3 attempts, 1s / 2s / 4s), buffer snapshots in memory, and deliver them on the next successful callback. Real-time collaboration is never blocked by callback failures.

Error Handling#

WebSocket Close Codes#

CodeMeaningClient Action
1000Normal closureSession ended normally
1001Going awayServer shutting down, reconnect
1008Policy violationInvalid message format, do not reconnect
1011Internal errorServer error, reconnect with backoff
4001UnauthorizedCollab token invalid/expired, re-authenticate
4002KickedUser removed from room, do not reconnect
4003Room closedDocument destroyed, do not reconnect
4004Room not foundRoom does not exist or has been destroyed
4009Already connectedDuplicate connection for same user+room
4029Rate limitedToo many messages, back off

WebSocket Error Messages#

Sent as JSON right before closing (when applicable):

Internal HTTP Errors#

StatusCodeWhenResponse
200Success (all endpoints except document creation)Typed response body
201Document createdPOST /internal/documents returns 201 on success
400COLLAB_INVALID_REQUESTInvalid request body (missing roomId, bad format)ErrorResponse
404COLLAB_DOCUMENT_NOT_FOUNDNo document exists for this roomErrorResponse
409COLLAB_DOCUMENT_ALREADY_EXISTSDocument already exists (create)ErrorResponse
500COLLAB_INTERNAL_ERRORUnexpected internal errorErrorResponse
Modified at 2026-03-12 05:29:50
Previous
Security
Next
Execution-Plane
Built with