Documentation Index Fetch the complete documentation index at: https://mintlify.com/tldraw/tldraw/llms.txt
Use this file to discover all available pages before exploring further.
Server API
Complete API reference for server-side multiplayer synchronization.
TLSocketRoom
Server-side room managing WebSocket connections and document synchronization.
import { TLSocketRoom } from '@tldraw/sync-core'
const room = new TLSocketRoom ( options )
Constructor parameters
schema
StoreSchema<R, any>
required
Store schema defining record types and migrations. import { createTLSchema } from '@tldraw/tlschema'
const room = new TLSocketRoom ({
schema: createTLSchema ()
})
Storage backend for persistence. See storage options . import { InMemorySyncStorage } from '@tldraw/sync-core'
storage : new InMemorySyncStorage ()
Optional logger for warnings and errors. log : {
warn : ( ... args ) => console . warn ( '[SYNC]' , ... args ),
error : ( ... args ) => console . error ( '[SYNC]' , ... args )
}
onSessionRemoved
(room: TLSocketRoom, args: SessionRemovedArgs) => void
Callback when a client disconnects. onSessionRemoved : ( room , { sessionId , numSessionsRemaining , meta }) => {
console . log ( `User ${ meta . userId } disconnected` )
if ( numSessionsRemaining === 0 ) {
room . close ()
}
}
onBeforeSendMessage
(args: MessageArgs) => void
Called before sending each message to a client. onBeforeSendMessage : ({ sessionId , message , stringified , meta }) => {
console . log ( `Sending ${ message . type } to ${ sessionId } , size: ${ stringified . length } ` )
}
Called when any client’s presence data changes. onPresenceChange : () => {
console . log ( 'Presence updated' )
}
Methods
handleNewSession
(options: NewSessionOptions) => TLSocketRoom
Register a new client session. room . handleNewSession ({
sessionId: 'unique-session-id' ,
socket: webSocketAdapter ,
meta: { userId: 'user-123' },
isReadonly: false
})
Parameters:
sessionId (string, required) - Unique session identifier
socket (TLRoomSocket, required) - WebSocket adapter
meta (SessionMeta, required) - Application-specific metadata
isReadonly (boolean, required) - Whether client can modify documents
handleMessage
(sessionId: string, message: TLSocketClientSentEvent) => Promise<void>
Process incoming message from a client. await room . handleMessage ( sessionId , message )
Handles:
connect - Initial handshake
push - Document changes
ping - Keep-alive
handleClose
(sessionId: string) => void
Handle client disconnection. room . handleClose ( sessionId )
sendCustomMessage
(sessionId: string, data: any) => void
Send custom message to specific client. room . sendCustomMessage ( 'session-123' , {
type: 'notification' ,
message: 'Document saved'
})
rejectSession
(sessionId: string, reason?: TLSyncErrorCloseEventReason) => void
Reject and disconnect a session. import { TLSyncErrorCloseEventReason } from '@tldraw/sync-core'
room . rejectSession (
'session-123' ,
TLSyncErrorCloseEventReason . FORBIDDEN
)
Close room and disconnect all clients.
Check if room is closed. if ( room . isClosed ()) {
console . log ( 'Room is closed' )
}
Events
events.on
(event: string, callback: Function) => () => void
Subscribe to room events. Returns unsubscribe function. const unsubscribe = room . events . on ( 'room_became_empty' , () => {
console . log ( 'Last user left' )
room . close ()
})
const unsubscribe2 = room . events . on ( 'session_removed' , ({ sessionId , meta }) => {
console . log ( `Session ${ sessionId } removed` )
})
Available events:
room_became_empty - Last client disconnected
session_removed - Client session ended
Example
import { WebSocketServer } from 'ws'
import { TLSocketRoom } from '@tldraw/sync-core'
import { createTLSchema } from '@tldraw/tlschema'
const wss = new WebSocketServer ({ port: 8080 })
const rooms = new Map < string , TLSocketRoom >()
wss . on ( 'connection' , ( ws , req ) => {
const url = new URL ( req . url ! , `wss:// ${ req . headers . host } ` )
const roomId = url . pathname . split ( '/' ). pop () !
const sessionId = url . searchParams . get ( 'sessionId' ) !
// Get or create room
let room = rooms . get ( roomId )
if ( ! room ) {
room = new TLSocketRoom ({
schema: createTLSchema (),
onSessionRemoved : ( room , { numSessionsRemaining }) => {
if ( numSessionsRemaining === 0 ) {
room . close ()
rooms . delete ( roomId )
}
},
log: {
warn : ( ... args ) => console . warn ( `[ ${ roomId } ]` , ... args ),
error : ( ... args ) => console . error ( `[ ${ roomId } ]` , ... args )
}
})
rooms . set ( roomId , room )
}
// Connect client
room . handleNewSession ({
sessionId ,
socket: ws ,
meta: { userId: 'anonymous' },
isReadonly: false
})
// Handle messages
ws . on ( 'message' , async ( data ) => {
const message = JSON . parse ( data . toString ())
await room . handleMessage ( sessionId , message )
})
// Handle disconnect
ws . on ( 'close' , () => {
room . handleClose ( sessionId )
})
})
TLSyncRoom
Lower-level room implementation. Most servers should use TLSocketRoom instead.
import { TLSyncRoom } from '@tldraw/sync-core'
const room = new TLSyncRoom ( options )
Constructor parameters
Similar to TLSocketRoom but without WebSocket handling:
schema
StoreSchema<R, any>
required
Store schema.
Presence change callback.
Methods
Provides lower-level control over sessions and messaging. See source code at /home/daytona/workspace/source/packages/sync-core/src/lib/TLSyncRoom.ts:147 for full API.
Storage
InMemorySyncStorage
In-memory storage for development.
import { InMemorySyncStorage } from '@tldraw/sync-core'
const storage = new InMemorySyncStorage ()
Loses all data on server restart. Only use for development.
SQLiteSyncStorage
SQLite-based persistent storage.
import { SQLiteSyncStorage , NodeSqliteWrapper } from '@tldraw/sync-core'
import Database from 'better-sqlite3'
const db = new Database ( './rooms.db' )
const sql = new NodeSqliteWrapper ( db )
const storage = new SQLiteSyncStorage ({ sql })
Parameters:
sql (TLSyncSqliteWrapper, required) - SQLite wrapper
snapshot (RoomSnapshot, optional) - Initial snapshot to load
DurableObjectSqliteSyncWrapper
SQLite wrapper for Cloudflare Durable Objects.
import { DurableObjectSqliteSyncWrapper } from '@tldraw/sync-core'
const sql = new DurableObjectSqliteSyncWrapper ( durableObject . ctx . storage )
const storage = new SQLiteSyncStorage ({ sql })
NodeSqliteWrapper
SQLite wrapper for Node.js with better-sqlite3.
import { NodeSqliteWrapper } from '@tldraw/sync-core'
import Database from 'better-sqlite3'
const db = new Database ( './room.db' )
const sql = new NodeSqliteWrapper ( db )
ServerSocketAdapter
WebSocket adapter for standard WebSocket libraries.
import { ServerSocketAdapter } from '@tldraw/sync-core'
import type { WebSocket } from 'ws'
const adapter = new ServerSocketAdapter ({
ws: websocket ,
onBeforeSendMessage : ({ message , stringified }) => {
console . log ( `Sending ${ message . type } ` )
}
})
Constructor parameters
WebSocket instance implementing minimal interface: interface WebSocketMinimal {
send ( data : string ) : void
close ( code ?: number , reason ?: string ) : void
addEventListener ( event : string , listener : Function ) : void
removeEventListener ( event : string , listener : Function ) : void
}
onBeforeSendMessage
(args: { message: TLSocketServerSentEvent, stringified: string }) => void
Called before sending each message.
Properties
Whether socket is currently open.
Methods
sendMessage
(msg: TLSocketServerSentEvent) => void
Send message to client.
close
(code?: number, reason?: string) => void
Close socket connection.
Types
Application-specific metadata attached to each session.
interface MySessionMeta {
userId : string
permissions : string []
connectedAt : number
}
const room = new TLSocketRoom < TLRecord , MySessionMeta >({
schema: createTLSchema (),
onSessionRemoved : ( room , { meta }) => {
console . log ( `User ${ meta . userId } disconnected` )
}
})
TLRoomSocket
Socket interface required by TLSocketRoom.
interface TLRoomSocket < R extends UnknownRecord > {
isOpen : boolean
sendMessage ( msg : TLSocketServerSentEvent < R >) : void
close ( code ?: number , reason ?: string ) : void
}
RoomSnapshot
Complete room state for persistence.
interface RoomSnapshot {
clock ?: number
documentClock ?: number
documents : Array <{
state : UnknownRecord
lastChangedClock : number
}>
tombstones ?: Record < string , number >
tombstoneHistoryStartsAtClock ?: number
schema ?: SerializedSchema
}
Protocol messages
Client → Server
type TLSocketClientSentEvent =
| { type : 'connect' ; connectRequestId : string ; schema : SerializedSchema ; protocolVersion : number ; lastServerClock : number }
| { type : 'push' ; clientClock : number ; diff ?: NetworkDiff ; presence ?: [ RecordOpType , ObjectDiff | R ] }
| { type : 'ping' }
Server → Client
type TLSocketServerSentEvent =
| { type : 'connect' ; connectRequestId : string ; hydrationType : 'wipe_all' | 'wipe_presence' ; protocolVersion : number ; schema : SerializedSchema ; serverClock : number ; diff : NetworkDiff ; isReadonly : boolean }
| { type : 'patch' ; diff : NetworkDiff ; serverClock : number }
| { type : 'push_result' ; clientClock : number ; serverClock : number ; action : 'commit' | 'discard' | { rebaseWithDiff : NetworkDiff } }
| { type : 'pong' }
| { type : 'data' ; data : TLSocketServerSentDataEvent [] }
| { type : 'custom' ; data : any }
Error codes
import { TLSyncErrorCloseEventCode , TLSyncErrorCloseEventReason } from '@tldraw/sync-core'
// Close code (4099)
TLSyncErrorCloseEventCode
// Error reasons
TLSyncErrorCloseEventReason . NOT_FOUND // 'NOT_FOUND'
TLSyncErrorCloseEventReason . FORBIDDEN // 'FORBIDDEN'
TLSyncErrorCloseEventReason . NOT_AUTHENTICATED // 'NOT_AUTHENTICATED'
TLSyncErrorCloseEventReason . UNKNOWN_ERROR // 'UNKNOWN_ERROR'
TLSyncErrorCloseEventReason . CLIENT_TOO_OLD // 'CLIENT_TOO_OLD'
TLSyncErrorCloseEventReason . SERVER_TOO_OLD // 'SERVER_TOO_OLD'
TLSyncErrorCloseEventReason . INVALID_RECORD // 'INVALID_RECORD'
TLSyncErrorCloseEventReason . RATE_LIMITED // 'RATE_LIMITED'
TLSyncErrorCloseEventReason . ROOM_FULL // 'ROOM_FULL'
Usage:
if ( ! hasPermission ( userId , roomId )) {
socket . close (
TLSyncErrorCloseEventCode ,
TLSyncErrorCloseEventReason . FORBIDDEN
)
}
See also
Client API Client-side API reference
Store sync Storage API reference
Setup guide Server setup guide
Authentication Add authentication