Multiplayer API

Everything for real-time collaborative whiteboards: component, adapter, types, server, persistence.


Component

ExcalidrawMultiplayer

Props: roomId: string, userInfo: RoomUserInfo, adapter: ExcalidrawMultiplayerAdapter, optional theme, UIOptions. For user identity UI (name + color picker), use CollaboratorProfile.

<script>
  import { ExcalidrawMultiplayer, createDefaultAdapter } from "svelte-excalidraw";

  const adapter = createDefaultAdapter({
    streamUrl: (roomId) => `/room/${roomId}`,
  });
</script>

<ExcalidrawMultiplayer
  roomId="my-room"
  userInfo={{ username: "Alice", color: { background: "#6965db", stroke: "#6965db" } }}
  {adapter}
/>

createDefaultAdapter

Returns ExcalidrawMultiplayerAdapter. Options: streamUrl: (roomId: string) => string (full URL for the room stream).

import { createDefaultAdapter } from "svelte-excalidraw";

const adapter = createDefaultAdapter({
  streamUrl: (roomId) => `/room/${roomId}`,
});

Types

ExcalidrawMultiplayerAdapter

Implement join and push for custom backends.

interface ExcalidrawMultiplayerAdapter {
  join(roomId: string, userInfo: RoomUserInfo): Promise<RoomConnection>;
  push(roomId: string, userId: string, payload: RoomPush): Promise<void>;
}

RoomUserInfo

interface RoomUserInfo {
  username: string;
  color?: { background: string; stroke: string };
  avatarUrl?: string;
}

RoomConnection

Returned by adapter.join().

interface RoomConnection {
  userId: string;
  subscribe(): AsyncIterable<RoomEvent>;
  leave(): void;
}

RoomPush

Payload for adapter.push(). Use switch (payload.type).

typePayload
"elements"{ elements }
"files"{ files }
"viewport"{ sceneBounds }
"followState"{ followingUserId: string | null }
"userInfo"{ userInfo: RoomUserInfo }
"awareness"{ awareness: AwarenessUpdate }

RoomEvent

Events from subscribe(). First is always init with document.

typePayload
"init"{ document, userId?, collaborators? }
"elements", "files", "viewport", "awareness"type-specific
"collaborator_joined", "collaborator_updated", "collaborator_left"(none)
"host_left", "followed_by", "sync"(none)

ExcalidrawStreamJoinOptions

Options for handleExcalidrawStream.

interface ExcalidrawStreamJoinOptions {
  roomId: string;
  username: string;
  color?: { background: string; stroke: string };
  initialDocument?: RoomDocument;
  onJoin?: (roomId: string, userId: string) => void;
  persist?: boolean;  // default true
}

Server

handleExcalidrawStream

Returns Promise<Response> (SSE). Use in your POST handler after auth/parsing.

// src/routes/room/[roomId]/+server.ts
import { handleExcalidrawStream } from "svelte-excalidraw/server";

export const prerender = false;

export async function POST({ request, params }) {
  const body = (await request.json().catch(() => ({}))) as { username?: string; color?: { background: string; stroke: string } };
  const roomId = params.roomId;
  const username = body.username?.trim();
  if (!roomId || !username)
    return new Response("Missing roomId or username", { status: 400 });
  return handleExcalidrawStream(request, { roomId, username, color: body.color });
}

Remotes

Register in SvelteKit so the default adapter can push: pushElements, pushFiles, pushViewport, pushFollowState, updateUserInfo, pushAwareness. From svelte-excalidraw/remote.


Persistence

  • setLoadCallback / setPersistCallback: Custom load/save. From svelte-excalidraw/server/state.
  • registerFileStorage: File-based persistence. From svelte-excalidraw/server/file-storage.
  • exportRoomToSinglePayload(roomId) / importRoomFromSinglePayload(roomId, payload): Backup/restore.
// src/hooks.server.ts
import { setLoadCallback, setPersistCallback } from "svelte-excalidraw/server/state";

setLoadCallback((roomId) => myDb.getRoom(roomId) ?? null);
setPersistCallback((roomId, doc) => myDb.saveRoom(roomId, doc));