Integration

Multiplayer with Liveblocks

Synchronize Telva documents and presence through Liveblocks using onChangePage and presence callbacks.

Multiplayer with Liveblocks

The reference example in examples/telva-example/src/multiplayer uses Liveblocks as transport/storage while Telva remains the editor runtime.

Core integration idea

  • send local shape/binding/asset patches from onChangePage
  • mirror cursor/selection presence through onChangePresence
  • rehydrate remote updates into the editor using replacePageContent

Minimal mount pattern

import { TVUserStatus, Telva } from 'telva'
import { RoomProvider } from './liveblocks.config'
import { useMultiplayerState } from './useMultiplayerState'

const roomId = 'mp-room'

export function Multiplayer() {
  return (
    <RoomProvider
      id={roomId}
      initialPresence={{
        id: 'DEFAULT_ID',
        user: {
          id: 'DEFAULT_ID',
          status: TVUserStatus.Connecting,
          activeShapes: [],
          color: 'black',
          point: [0, 0],
          selectedIds: [],
        },
      }}
    >
      <Editor roomId={roomId} />
    </RoomProvider>
  )
}

function Editor({ roomId }: { roomId: string }) {
  const { error, ...events } = useMultiplayerState(roomId)
  if (error) return <div>Error: {error.message}</div>

  return <Telva {...events} disableAssets showPages={false} />
}

Why disableAssets is often enabled

In multiplayer, unmanaged media may write large base64 payloads to shared storage.

For production rooms:

  • keep disableAssets={true} by default, or
  • implement object storage and return URL-backed assets via onAssetCreate / onAssetUpload

Session-aware history management

The example pauses room history on session start and resumes it on session end:

const onSessionStart = () => {
  room.history.pause()
}

const onSessionEnd = () => {
  room.history.resume()
}

This prevents noisy intermediate transforms from polluting collaborative history.

Data model used in the example

Liveblocks storage keys:

  • shapes: LiveMap<string, TVShape>
  • bindings: LiveMap<string, TVBinding>
  • assets: LiveMap<string, TVAsset>
  • version: number for migration logic

On startup, the hook initializes maps, runs migration fallback if needed, subscribes to map changes, and calls app.replacePageContent(...).

On this page