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.
Client API
Complete API reference for client-side multiplayer synchronization.
useSync
React hook for creating a synchronized store with multiplayer support.
import { useSync } from '@tldraw/sync'
const store = useSync ( options )
Parameters
uri
string | () => string | Promise<string>
required
WebSocket server URI for synchronization. Can be:
Static string: 'wss://sync.example.com/room-123'
Sync function: () => 'wss://sync.example.com/room-123'
Async function: async () => { const token = await getToken(); return \wss://…?token=$` }`
Reserved query parameters sessionId and storeId are automatically added.
Asset store implementation for file uploads and storage. Must implement:
upload(asset, file): Promise<{ src: string }> - Upload file and return URL
resolve(asset, context): string | null - Resolve asset URL for display
assets : {
async upload ( asset , file ) {
const formData = new FormData ()
formData . append ( 'file' , file )
const res = await fetch ( '/api/upload' , { method: 'POST' , body: formData })
const { url } = await res . json ()
return { src: url }
},
resolve ( asset ) {
return asset . props . src
}
}
userInfo
TLPresenceUserInfo | Signal<TLPresenceUserInfo>
User information for presence system. userInfo : {
id : 'user-123' ,
name : 'Alice' ,
color : '#ff0000'
}
Can be a reactive signal for dynamic updates: const user = atom ( 'user' , { id: 'user-123' , name: 'Alice' , color: '#ff0000' })
userInfo : user
getUserPresence
(store: TLStore, user: TLPresenceUserInfo) => TLPresenceStateInfo | null
Customize presence data broadcast to other clients. getUserPresence : ( store , user ) => {
const instance = store . get ( 'instance:instance' )
if ( ! instance ) return null
return {
userId: user . id ,
userName: user . name ,
cursor: instance . cursor ,
selectedShapeIds: instance . selectedShapeIds ,
currentTool: instance . currentToolId
}
}
Return null to hide presence from other users.
Handler for custom messages from other clients or server. onCustomMessageReceived : ( data ) => {
if ( data . type === 'chat' ) {
console . log ( ` ${ data . user } : ${ data . message } ` )
}
}
Custom shape utilities to include in the schema.
Custom binding utilities to include in the schema.
Return value
RemoteTLStoreWithStatus - A reactive store wrapper with connection status.
status
'loading' | 'synced-remote' | 'error'
Current synchronization state:
loading - Establishing connection and loading initial state
synced-remote - Connected and actively synchronizing
error - Connection failed or sync error occurred
The synchronized tldraw store (only present when status === 'synced-remote').
Real-time connection status (only present when status === 'synced-remote'):
online - Connected to server
offline - Temporarily disconnected, will reconnect automatically
Error object with message (only present when status === 'error').
Example
import { Tldraw } from 'tldraw'
import { useSync } from '@tldraw/sync'
import 'tldraw/tldraw.css'
function MyApp ({ roomId } : { roomId : string }) {
const store = useSync ({
uri: `wss://sync.example.com/room/ ${ roomId } ` ,
assets: {
async upload ( asset , file ) {
const formData = new FormData ()
formData . append ( 'file' , file )
const res = await fetch ( '/api/upload' , {
method: 'POST' ,
body: formData
})
const { url } = await res . json ()
return { src: url }
},
resolve ( asset ) {
return asset . props . src
}
},
userInfo: {
id: 'user-123' ,
name: 'Alice' ,
color: '#ff0000'
},
getUserPresence : ( store , user ) => {
const instance = store . get ( 'instance:instance' )
if ( ! instance ) return null
return {
userId: user . id ,
userName: user . name ,
cursor: instance . cursor ,
selectedShapeIds: instance . selectedShapeIds
}
},
onCustomMessageReceived : ( data ) => {
if ( data . type === 'notification' ) {
alert ( data . message )
}
}
})
if ( store . status === 'loading' ) {
return < div > Loading... </ div >
}
if ( store . status === 'error' ) {
return < div > Error: { store . error . message } </ div >
}
return < Tldraw store = { store . store } />
}
useSyncDemo
Quick setup hook using tldraw’s demo server. For testing only.
import { useSyncDemo } from '@tldraw/sync'
const store = useSyncDemo ({ roomId: 'my-test-room' })
Data on demo server is deleted after 24 hours. Rooms are publicly accessible. Use your own server for production.
Parameters
Unique room identifier. Use a company/project prefix to avoid collisions. // Good - unique prefix
roomId : 'acme-design-review-2024'
// Better - UUID for privacy
roomId : `acme- ${ nanoid () } `
// Bad - too generic
roomId : 'room1'
userInfo
TLPresenceUserInfo | Signal<TLPresenceUserInfo>
User information for presence. Same as useSync.
getUserPresence
(store: TLStore, user: TLPresenceUserInfo) => TLPresenceStateInfo | null
Customize presence data. Same as useSync.
Override demo server URL. Defaults to https://demo.tldraw.xyz.
Return value
Same as useSync - RemoteTLStoreWithStatus.
Example
import { Tldraw } from 'tldraw'
import { useSyncDemo } from '@tldraw/sync'
import 'tldraw/tldraw.css'
function QuickDemo () {
const store = useSyncDemo ({
roomId: 'my-company-test-room' ,
userInfo: {
id: 'user-' + Math . random (),
name: 'Test User' ,
color: '#' + Math . floor ( Math . random () * 16777215 ). toString ( 16 )
}
})
if ( store . status === 'loading' ) {
return < div > Connecting... </ div >
}
if ( store . status === 'error' ) {
return < div > Error: { store . error . message } </ div >
}
return < Tldraw store = { store . store } />
}
TLSyncClient
Low-level sync client for advanced use cases. Most apps should use useSync instead.
import { TLSyncClient } from '@tldraw/sync-core'
const client = new TLSyncClient ( config )
Constructor parameters
The local tldraw store to synchronize.
socket
TLPersistentClientSocket
required
WebSocket adapter implementing the persistent socket interface. interface TLPersistentClientSocket {
connectionStatus : 'online' | 'offline' | 'error'
sendMessage ( msg : TLSocketClientSentEvent ) : void
onReceiveMessage : ( callback : ( msg : TLSocketServerSentEvent ) => void ) => () => void
onStatusChange : ( callback : ( event : TLSocketStatusChangeEvent ) => void ) => () => void
restart () : void
close () : void
}
presence
Signal<TLRecord | null>
required
Reactive signal containing presence state to broadcast.
Control presence sharing mode:
solo - No presence shared, sync at 1 FPS
full - Full presence shared, sync at 30 FPS
onLoad
(client: TLSyncClient) => void
required
Callback when initial synchronization completes.
onSyncError
(reason: string) => void
required
Callback when synchronization fails with error reason.
Handler for custom messages.
onAfterConnect
(client: TLSyncClient, details: { isReadonly: boolean }) => void
Callback after successful connection with connection details.
Methods
Close the sync client and clean up resources. Cannot be reused after closing.
Example
import { TLSyncClient , ClientWebSocketAdapter } from '@tldraw/sync-core'
import { createTLStore , atom } from 'tldraw'
const store = createTLStore ({ schema: mySchema })
const socket = new ClientWebSocketAdapter (
() => 'wss://sync.example.com/room'
)
const presence = atom ( 'presence' , null )
const client = new TLSyncClient ({
store ,
socket ,
presence ,
onLoad : ( client ) => {
console . log ( 'Loaded and synced' )
},
onSyncError : ( reason ) => {
console . error ( 'Sync error:' , reason )
},
onAfterConnect : ( client , { isReadonly }) => {
console . log ( 'Connected, readonly:' , isReadonly )
}
})
// Clean up
client . close ()
ClientWebSocketAdapter
Built-in WebSocket adapter with automatic reconnection.
import { ClientWebSocketAdapter } from '@tldraw/sync-core'
const adapter = new ClientWebSocketAdapter ( getUri )
Constructor parameters
getUri
() => string | Promise<string>
required
Function that returns WebSocket URI. Called on each connection attempt. new ClientWebSocketAdapter ( async () => {
const token = await getAuthToken ()
return `wss://sync.example.com/room?token= ${ token } `
})
Properties
connectionStatus
'online' | 'offline' | 'error'
Current connection state.
Methods
sendMessage
(msg: TLSocketClientSentEvent) => void
Send message to server.
onReceiveMessage
(callback: (msg: TLSocketServerSentEvent) => void) => () => void
Subscribe to messages from server. Returns unsubscribe function.
onStatusChange
(callback: (event: TLSocketStatusChangeEvent) => void) => () => void
Subscribe to connection status changes. Returns unsubscribe function.
Force connection restart (disconnect then reconnect).
Close connection permanently.
Types
RemoteTLStoreWithStatus
Store wrapper returned by useSync and useSyncDemo.
type RemoteTLStoreWithStatus =
| { status : 'loading' }
| {
status : 'synced-remote'
store : TLStore
connectionStatus : 'online' | 'offline'
}
| {
status : 'error'
error : Error
}
TLPresenceUserInfo
User information for presence system.
interface TLPresenceUserInfo {
id : string
name ?: string
color ?: string
// ... other custom fields
}
TLPresenceStateInfo
Presence state to broadcast.
interface TLPresenceStateInfo {
userId : string
userName ?: string
cursor : { x : number ; y : number } | null
selectedShapeIds : string []
// ... other custom fields
}
TLAssetStore
Asset storage interface.
interface TLAssetStore {
upload (
asset : TLAsset ,
file : File
) : Promise <{ src : string }>
resolve (
asset : TLAsset ,
context : {
networkEffectiveType ?: string
shouldResolveToOriginal : boolean
steppedScreenScale : number
dpr : number
}
) : string | null
}
Error codes
Sync errors with specific close event reasons:
import { TLSyncErrorCloseEventReason } from '@tldraw/sync-core'
TLSyncErrorCloseEventReason . NOT_FOUND // Room not found
TLSyncErrorCloseEventReason . FORBIDDEN // Access denied
TLSyncErrorCloseEventReason . NOT_AUTHENTICATED // Auth required
TLSyncErrorCloseEventReason . CLIENT_TOO_OLD // Client version too old
TLSyncErrorCloseEventReason . SERVER_TOO_OLD // Server version too old
TLSyncErrorCloseEventReason . INVALID_RECORD // Invalid data sent
TLSyncErrorCloseEventReason . RATE_LIMITED // Too many requests
TLSyncErrorCloseEventReason . ROOM_FULL // Room at capacity
Handle in error state:
if ( store . status === 'error' ) {
const msg = store . error . message
if ( msg . includes ( 'NOT_FOUND' )) {
return < div > Room not found </ div >
}
if ( msg . includes ( 'CLIENT_TOO_OLD' )) {
return < div > Please refresh to update </ div >
}
// ... handle other errors
}
See also
Server API Server-side API reference
Store sync Store synchronization API
Setup guide Production setup guide
Customization Customize sync behavior