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
| Pattern | API |
|---|
| Attach user info to connection | connection.setState({ name, color, cursor }) |
| Enumerate all connected users | this.getConnections<State>() |
| Read a user’s stored state | connection.state |
| Filter by role / tag | getConnectionTags() + getConnections("tag") |
| Broadcast delta | this.broadcast(msg, [connection.id]) |
| Send to one user | connection.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.