Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/cloudflare/partykit/llms.txt

Use this file to discover all available pages before exploring further.

Presence is the feature that makes a collaborative app feel alive: avatars in the toolbar, live cursors moving across a canvas, a list of who’s currently reading a document. PartyServer provides the primitives you need — connection enumeration, per-connection state, and tagged filtering — to build rich presence without any additional infrastructure.

What presence means in PartyServer

Every WebSocket connection to a PartyServer DO has a stable id and an optional state blob (up to 2 KB). The server can iterate over all live connections with getConnections(), read their state, and broadcast targeted updates. Combining these three capabilities covers almost every presence scenario.

Core patterns

Storing per-connection state

Use connection.setState() in onConnect to attach presence data — a user name, avatar URL, cursor position, or anything else — directly to the connection object.
import type { Connection, ConnectionContext } from "partyserver";

type PresenceState = {
  userId: string;
  name: string;
  color: string;
  cursor: { x: number; y: number } | null;
};

onConnect(connection: Connection<PresenceState>, ctx: ConnectionContext) {
  const url = new URL(ctx.request.url);
  connection.setState({
    userId: url.searchParams.get("userId") ?? connection.id,
    name: url.searchParams.get("name") ?? "Anonymous",
    color: url.searchParams.get("color") ?? "#888888",
    cursor: null,
  });
}

Sending the full presence list on connect

When a new client joins, send them the state of every existing connection so they can render the current presence immediately.
onConnect(connection: Connection<PresenceState>, ctx: ConnectionContext) {
  // ... setState as above ...

  // Build the current presence list from all live connections
  const presenceList = [...this.getConnections<PresenceState>()]
    .filter((c) => c.id !== connection.id && c.state !== null)
    .map((c) => ({ id: c.id, ...c.state! }));

  connection.send(JSON.stringify({ type: "presence-init", users: presenceList }));

  // Announce the new user to everyone already connected
  this.broadcast(
    JSON.stringify({ type: "presence-join", id: connection.id, ...connection.state }),
    [connection.id]
  );
}

Broadcasting cursor delta updates

When a client sends a cursor move, update the stored state and re-broadcast only the delta — not the full list — to keep bandwidth low.
onMessage(connection: Connection<PresenceState>, message: string) {
  const data = JSON.parse(message);

  if (data.type === "cursor-move") {
    // Merge cursor into existing state
    connection.setState({
      ...connection.state!,
      cursor: { x: data.x, y: data.y },
    });

    // Broadcast only the delta to other connections
    this.broadcast(
      JSON.stringify({
        type: "cursor-move",
        id: connection.id,
        x: data.x,
        y: data.y,
      }),
      [connection.id] // exclude the sender
    );
  }
}

Cleaning up on disconnect

Remove the disconnecting user from the presence list and broadcast a departure event.
onClose(connection: Connection<PresenceState>) {
  this.broadcast(
    JSON.stringify({ type: "presence-leave", id: connection.id }),
    [connection.id]
  );
}

Filtering connections by tag

getConnectionTags lets you attach string tags to a connection (for example, user roles or room membership). You can then call getConnections("admin") to iterate only over tagged connections.
getConnectionTags(connection: Connection, ctx: ConnectionContext): string[] {
  const url = new URL(ctx.request.url);
  const role = url.searchParams.get("role") ?? "viewer";
  return [role]; // e.g. "editor", "viewer", "admin"
}

// Later — broadcast only to editors
broadcastToEditors(message: string) {
  for (const conn of this.getConnections("editor")) {
    conn.send(message);
  }
}

Full presence server example

// src/server.ts
import { routePartykitRequest, Server } from "partyserver";
import type { Connection, ConnectionContext, WSMessage } from "partyserver";
import { env } from "cloudflare:workers";

type PresenceState = {
  name: string;
  color: string;
  cursor: { x: number; y: number } | null;
};

export class PresenceRoom extends Server {
  static options = { hibernate: true };

  getConnectionTags(connection: Connection, ctx: ConnectionContext): string[] {
    const url = new URL(ctx.request.url);
    return [url.searchParams.get("role") ?? "viewer"];
  }

  onConnect(connection: Connection<PresenceState>, ctx: ConnectionContext) {
    const url = new URL(ctx.request.url);

    connection.setState({
      name: url.searchParams.get("name") ?? "Anonymous",
      color: url.searchParams.get("color") ?? "#888888",
      cursor: null,
    });

    // Send the full presence list to the newcomer
    const users = [...this.getConnections<PresenceState>()]
      .filter((c) => c.id !== connection.id && c.state)
      .map((c) => ({ id: c.id, ...c.state! }));

    connection.send(JSON.stringify({ type: "presence-init", users }));

    // Tell everyone else about the new user
    this.broadcast(
      JSON.stringify({ type: "presence-join", id: connection.id, ...connection.state }),
      [connection.id]
    );
  }

  onMessage(connection: Connection<PresenceState>, message: WSMessage) {
    const data = JSON.parse(message as string);

    if (data.type === "cursor-move") {
      connection.setState({ ...connection.state!, cursor: { x: data.x, y: data.y } });

      this.broadcast(
        JSON.stringify({ type: "cursor-move", id: connection.id, x: data.x, y: data.y }),
        [connection.id]
      );
    }
  }

  onClose(connection: Connection<PresenceState>) {
    this.broadcast(
      JSON.stringify({ type: "presence-leave", id: connection.id }),
      [connection.id]
    );
  }
}

export default {
  async fetch(request: Request): Promise<Response> {
    return (
      (await routePartykitRequest(request, env)) ||
      new Response("Not Found", { status: 404 })
    );
  },
} satisfies ExportedHandler<Env>;

Client implementation

// src/client.ts
import { PartySocket } from "partysocket";

type User = {
  id: string;
  name: string;
  color: string;
  cursor: { x: number; y: number } | null;
};

const users = new Map<string, User>();

const socket = new PartySocket({
  host: "https://my-app.workers.dev",
  party: "presence-room",
  room: "canvas-1",
  query: { name: "Alice", color: "#FF6B6B", role: "editor" },
});

socket.addEventListener("message", (event) => {
  const msg = JSON.parse(event.data as string);

  switch (msg.type) {
    case "presence-init":
      // Seed the users map with everyone already in the room
      for (const user of msg.users) users.set(user.id, user);
      renderPresence();
      break;

    case "presence-join":
      users.set(msg.id, msg);
      renderPresence();
      break;

    case "presence-leave":
      users.delete(msg.id);
      renderPresence();
      break;

    case "cursor-move":
      const user = users.get(msg.id);
      if (user) user.cursor = { x: msg.x, y: msg.y };
      renderCursors();
      break;
  }
});

// Send cursor position on mouse move
document.addEventListener("mousemove", (e) => {
  socket.send(JSON.stringify({ type: "cursor-move", x: e.clientX, y: e.clientY }));
});

Presence patterns at a glance

PatternAPI
Attach user info to connectionconnection.setState({ name, color, cursor })
Enumerate all connected usersthis.getConnections<State>()
Read a user’s stored stateconnection.state
Filter by role / taggetConnectionTags() + getConnections("tag")
Broadcast deltathis.broadcast(msg, [connection.id])
Send to one userconnection.send(msg)
connection.state survives WebSocket hibernation — when the DO wakes up from sleep, every connection’s state is restored automatically. You don’t need to reload it from external storage.

Real-world reference

The fixtures/globe fixture is an excellent production-quality example of the presence pattern. It uses conn.setState({ position }) to store each visitor’s geographic coordinates (read from Cloudflare’s request.cf headers), iterates getConnections() to send a full snapshot on join, and cleans up in onClose — all in under 80 lines of TypeScript.

Build docs developers (and LLMs) love