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.
Chat rooms and Durable Objects are a natural pair. Every room is a named DO instance, so routing is trivial — the room ID in the URL becomes the DO name, and all messages for that room flow through one place. PartyServer handles the WebSocket lifecycle, leaving you to focus on message storage and broadcast logic.
How it works
A client connects
onConnect fires. You fetch recent messages from DO storage and send them to the new user so they see history immediately.
A user sends a message
onMessage fires. You persist the message to storage, then broadcast it to every connected client including the sender.
A user disconnects
onClose fires. You broadcast a departure notification so the room knows who left.
Server implementation
Define your message types
// src/types.ts
export type ChatMessage = {
type: "chat-message";
id: string;
content: string;
sender: string;
};
export type SystemMessage = {
type: "join" | "leave";
id: string;
sender: string;
};
export type Message = ChatMessage | SystemMessage;
Full server code
// src/server.ts
import { routePartykitRequest, Server } from "partyserver";
import type { Connection, ConnectionContext, WSMessage } from "partyserver";
import { env } from "cloudflare:workers";
import type { Message, ChatMessage } from "./types";
const MAX_MESSAGES = 50; // messages to keep in storage
export class Chat extends Server {
static options = { hibernate: true };
// ----------------------------------------------------------------
// onConnect: send message history to the new client
// ----------------------------------------------------------------
async onConnect(connection: Connection, ctx: ConnectionContext) {
const url = new URL(ctx.request.url);
const sender = url.searchParams.get("name") ?? "Anonymous";
// Attach the sender name to this connection so we can use it later
connection.setState({ sender });
// Load recent messages from storage and replay them
const history =
(await this.ctx.storage.get<ChatMessage[]>("messages")) ?? [];
connection.send(JSON.stringify({ type: "history", messages: history }));
// Announce arrival to everyone already in the room
this.broadcast(
JSON.stringify({ type: "join", id: connection.id, sender } satisfies Message),
[connection.id]
);
}
// ----------------------------------------------------------------
// onMessage: persist and broadcast every chat message
// ----------------------------------------------------------------
async onMessage(connection: Connection<{ sender: string }>, message: WSMessage) {
const data = JSON.parse(message as string) as ChatMessage;
// Persist to Durable Object storage
const history =
(await this.ctx.storage.get<ChatMessage[]>("messages")) ?? [];
history.push(data);
// Keep only the most recent messages to bound storage size
if (history.length > MAX_MESSAGES) history.splice(0, history.length - MAX_MESSAGES);
await this.ctx.storage.put("messages", history);
// Broadcast to everyone, including the sender
this.broadcast(JSON.stringify(data));
}
// ----------------------------------------------------------------
// onClose: notify remaining participants
// ----------------------------------------------------------------
onClose(connection: Connection<{ sender: string }>) {
const sender = connection.state?.sender ?? "Someone";
this.broadcast(
JSON.stringify({ type: "leave", id: connection.id, sender } satisfies Message),
[connection.id]
);
}
}
export default {
async fetch(request: Request): Promise<Response> {
return (
(await routePartykitRequest(request, env)) ||
new Response("Not Found", { status: 404 })
);
},
} satisfies ExportedHandler<Env>;
Enable hibernate: true so the DO can sleep between messages. All lifecycle hooks fire correctly when it wakes up, and you’re only billed for active CPU time.
Wrangler configuration
// wrangler.jsonc
{
"name": "my-chat-app",
"main": "src/server.ts",
"durable_objects": {
"bindings": [
{
"name": "Chat",
"class_name": "Chat"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["Chat"]
}
]
}
Client implementation
Vanilla TypeScript
// src/client.ts
import { PartySocket } from "partysocket";
const socket = new PartySocket({
host: "https://my-chat-app.workers.dev",
party: "chat", // kebab-cased binding name
room: "general", // room name = DO instance name
query: { name: "Alice" },
});
socket.addEventListener("open", () => {
console.log("Connected to room");
});
socket.addEventListener("message", (event) => {
const msg = JSON.parse(event.data as string);
if (msg.type === "history") {
// Render previous messages on initial load
renderHistory(msg.messages);
} else if (msg.type === "chat-message") {
appendMessage(msg);
} else if (msg.type === "join") {
appendSystem(`${msg.sender} joined`);
} else if (msg.type === "leave") {
appendSystem(`${msg.sender} left`);
}
});
// Send a chat message
function sendMessage(content: string) {
socket.send(
JSON.stringify({
type: "chat-message",
id: crypto.randomUUID(),
content,
sender: "Alice",
})
);
}
React with usePartySocket
// src/App.tsx
import { useState, useRef } from "react";
import { usePartySocket } from "partysocket/react";
import type { ChatMessage } from "./types";
export default function App() {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const inputRef = useRef<HTMLInputElement>(null);
const socket = usePartySocket({
party: "chat",
room: "general",
query: { name: "Alice" },
onMessage(evt) {
const msg = JSON.parse(evt.data as string);
if (msg.type === "history") {
setMessages(msg.messages);
} else if (msg.type === "chat-message") {
setMessages((prev) => [...prev, msg]);
}
},
});
function send() {
if (!inputRef.current?.value) return;
socket.send(
JSON.stringify({
type: "chat-message",
id: crypto.randomUUID(),
content: inputRef.current.value,
sender: "Alice",
})
);
inputRef.current.value = "";
}
return (
<>
<ul>
{messages.map((m) => (
<li key={m.id}>
<b>{m.sender}:</b> {m.content}
</li>
))}
</ul>
<input ref={inputRef} onKeyDown={(e) => e.key === "Enter" && send()} />
</>
);
}
Key patterns at a glance
| Pattern | How |
|---|
| Message history | ctx.storage.get / put in onConnect and onMessage |
| Broadcast to room | this.broadcast(message) — sends to all connected clients |
| Exclude sender | this.broadcast(message, [connection.id]) |
| Join / leave events | onConnect and onClose with system message broadcast |
| Player identity | connection.setState({ sender }) in onConnect |
Reference fixture
The fixtures/chat directory in the PartyKit repository is a minimal but fully working chat app that demonstrates usePartySocket, JSON message serialization, and the broadcast pattern. It is a good starting point for any room-based messaging feature.