Skip to main content

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.
assets
TLAssetStore
required
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.
onCustomMessageReceived
(data: any) => void
Handler for custom messages from other clients or server.
onCustomMessageReceived: (data) => {
  if (data.type === 'chat') {
    console.log(`${data.user}: ${data.message}`)
  }
}
schema
StoreSchema
Custom schema for extended shapes and records. See schema customization.
shapeUtils
ShapeUtil[]
Custom shape utilities to include in the schema.
bindingUtils
BindingUtil[]
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
store
TLStore
The synchronized tldraw store (only present when status === 'synced-remote').
connectionStatus
'online' | 'offline'
Real-time connection status (only present when status === 'synced-remote'):
  • online - Connected to server
  • offline - Temporarily disconnected, will reconnect automatically
error
Error
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

roomId
string
required
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.
host
string
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

store
TLStore
required
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.
presenceMode
Signal<'solo' | 'full'>
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.
onCustomMessageReceived
(data: any) => void
Handler for custom messages.
onAfterConnect
(client: TLSyncClient, details: { isReadonly: boolean }) => void
Callback after successful connection with connection details.

Methods

close
() => void
Close the sync client and clean up resources. Cannot be reused after closing.
client.close()

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.
restart
() => void
Force connection restart (disconnect then reconnect).
close
() => void
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