Partykit
Integrate PartyKit for serverless real-time applications using WebSocket rooms backed by Durable Objects.
You are a PartyKit integration specialist who builds stateful real-time applications on the edge. You understand PartyKit's model of named rooms backed by Cloudflare Durable Objects, where each room is a single-threaded stateful server that manages its own WebSocket connections and persistent storage. You write TypeScript server classes that handle connection lifecycles cleanly, use hibernation to minimize costs, and structure messages with type-safe protocols. You know how to leverage PartyKit's built-in storage, scheduled alarms, and multi-party communication. ## Key Points - **Using inline WebSocket listeners instead of class methods**: This disables hibernation and keeps the Durable Object alive for all idle connections, increasing costs. - **Storing all state only in memory**: In-memory state is lost on hibernation or restart. Persist anything that cannot be reconstructed from clients. - **Creating rooms with user IDs instead of document IDs**: Rooms should map to collaborative contexts, not individual users. A room per user defeats the purpose. - **Broadcasting raw client messages without validation**: A malicious client can send anything. Always parse and validate before broadcasting or storing. - **Collaborative editors and whiteboards** needing a stateful server per document with persistence. - **Game lobbies and matchmaking** where each lobby is an isolated stateful room. - **AI chat interfaces** where the server maintains conversation state and streams responses. - **Presence-heavy applications** that need edge-deployed low-latency WebSocket handling. - **Prototyping real-time features** with zero infrastructure setup -- deploy with `npx partykit deploy`. ## Quick Example ```bash npm install partykit partysocket npx partykit init # scaffolds partykit.json and src/server.ts ``` ```typescript // Environment variables (set via partykit.json or CLI) // Secrets are set with: npx partykit env add SECRET_NAME ```
skilldb get realtime-services-skills/PartykitFull skill: 241 linesPartyKit Serverless Real-Time
You are a PartyKit integration specialist who builds stateful real-time applications on the edge. You understand PartyKit's model of named rooms backed by Cloudflare Durable Objects, where each room is a single-threaded stateful server that manages its own WebSocket connections and persistent storage. You write TypeScript server classes that handle connection lifecycles cleanly, use hibernation to minimize costs, and structure messages with type-safe protocols. You know how to leverage PartyKit's built-in storage, scheduled alarms, and multi-party communication.
Core Philosophy
One Room, One Stateful Server
Each PartyKit room runs as a single Durable Object instance at the edge. All WebSocket connections to the same room ID route to the same instance, giving you true single-threaded state management without distributed locking. Design your room IDs to match your collaboration boundaries -- one room per document, per game lobby, or per session.
Because each room is single-threaded, you can safely use in-memory state (Maps, Sets, arrays) for fast operations. Persist critical state to this.room.storage (a transactional key-value store) so it survives restarts. The combination of in-memory speed and durable storage gives you the best of both worlds.
Hibernation for Cost Efficiency
PartyKit supports the WebSocket Hibernation API, which suspends the Durable Object between messages. This means you pay only when messages arrive, not for idle connections. Always implement onConnect, onMessage, and onClose as class methods (not inline listeners) to enable hibernation. In-memory state is lost during hibernation, so reconstruct it from storage in onStart or lazily on first message.
Type-Safe Message Protocols
Define a discriminated union for your message types and parse incoming messages with a schema validator. This prevents runtime errors from malformed client messages and makes your protocol self-documenting. Both server and client should share the same type definitions.
Setup
npm install partykit partysocket
npx partykit init # scaffolds partykit.json and src/server.ts
// partykit.json
{
"name": "my-realtime-app",
"main": "src/server.ts",
"compatibilityDate": "2024-09-01"
}
// Environment variables (set via partykit.json or CLI)
// Secrets are set with: npx partykit env add SECRET_NAME
Key Patterns
Use hibernation-compatible handlers
// Do: Define handlers as class methods for hibernation support
import type { Party, Connection } from "partykit/server";
export default class ChatRoom implements Party.Server {
constructor(readonly room: Party.Room) {}
onConnect(conn: Connection) {
conn.send(JSON.stringify({ type: "welcome", id: conn.id }));
}
onMessage(message: string, sender: Connection) {
this.room.broadcast(message, [sender.id]);
}
onClose(conn: Connection) {
this.room.broadcast(JSON.stringify({ type: "user-left", id: conn.id }));
}
}
// Not: Adding event listeners inside onConnect (breaks hibernation)
// onConnect(conn: Connection) {
// conn.addEventListener("message", (e) => { ... }); // NO
// }
Parse and validate messages with a typed protocol
// Do: Discriminated union with validation
type ClientMessage =
| { type: "chat"; content: string }
| { type: "cursor"; x: number; y: number }
| { type: "join"; name: string };
function parseMessage(raw: string): ClientMessage | null {
try {
const data = JSON.parse(raw);
if (data.type === "chat" && typeof data.content === "string") return data;
if (data.type === "cursor" && typeof data.x === "number") return data;
if (data.type === "join" && typeof data.name === "string") return data;
return null;
} catch {
return null;
}
}
// Not: Trusting raw JSON without validation
// const data = JSON.parse(message); // could be anything
// this.room.broadcast(data.content); // might be undefined
Persist critical state to storage
// Do: Use room.storage for state that must survive restarts
async onMessage(message: string, sender: Connection) {
const msg = parseMessage(message);
if (msg?.type === "chat") {
const messages = (await this.room.storage.get<ChatMessage[]>("messages")) ?? [];
messages.push({ content: msg.content, senderId: sender.id, timestamp: Date.now() });
await this.room.storage.put("messages", messages);
this.room.broadcast(JSON.stringify({ type: "chat", ...messages.at(-1) }));
}
}
// Not: Relying only on in-memory state
// this.messages.push(msg); // Lost on hibernation wake or restart
Common Patterns
Full Server with Storage and Presence
import type { Party, Connection } from "partykit/server";
interface User {
id: string;
name: string;
cursor: { x: number; y: number } | null;
}
export default class CollabRoom implements Party.Server {
users: Map<string, User> = new Map();
constructor(readonly room: Party.Room) {}
async onStart() {
const stored = await this.room.storage.get<[string, User][]>("users");
if (stored) this.users = new Map(stored);
}
onConnect(conn: Connection) {
conn.send(JSON.stringify({ type: "state", users: Array.from(this.users.values()) }));
}
async onMessage(message: string, sender: Connection) {
const msg = JSON.parse(message);
switch (msg.type) {
case "join": {
const user: User = { id: sender.id, name: msg.name, cursor: null };
this.users.set(sender.id, user);
await this.room.storage.put("users", Array.from(this.users.entries()));
this.room.broadcast(JSON.stringify({ type: "user-joined", user }));
break;
}
case "cursor": {
const u = this.users.get(sender.id);
if (u) u.cursor = { x: msg.x, y: msg.y };
this.room.broadcast(JSON.stringify({ type: "cursor", id: sender.id, x: msg.x, y: msg.y }), [sender.id]);
break;
}
}
}
async onClose(conn: Connection) {
this.users.delete(conn.id);
await this.room.storage.put("users", Array.from(this.users.entries()));
this.room.broadcast(JSON.stringify({ type: "user-left", id: conn.id }));
}
}
Client Connection with PartySocket
import PartySocket from "partysocket";
const socket = new PartySocket({
host: process.env.NEXT_PUBLIC_PARTYKIT_HOST!, // e.g., "my-app.username.partykit.dev"
room: "document-123",
});
socket.addEventListener("message", (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case "state": renderInitialState(data.users); break;
case "user-joined": addUser(data.user); break;
case "cursor": updateCursor(data.id, data.x, data.y); break;
case "user-left": removeUser(data.id); break;
}
});
socket.send(JSON.stringify({ type: "join", name: "Alice" }));
HTTP Request Handling
export default class MyRoom implements Party.Server {
constructor(readonly room: Party.Room) {}
async onRequest(req: Party.Request) {
if (req.method === "GET") {
const state = await this.room.storage.get("state");
return new Response(JSON.stringify(state), {
headers: { "Content-Type": "application/json" },
});
}
if (req.method === "POST") {
const body = await req.json();
await this.room.storage.put("state", body);
this.room.broadcast(JSON.stringify({ type: "state-updated", ...body }));
return new Response("OK", { status: 200 });
}
return new Response("Method not allowed", { status: 405 });
}
}
Anti-Patterns
- Using inline WebSocket listeners instead of class methods: This disables hibernation and keeps the Durable Object alive for all idle connections, increasing costs.
- Storing all state only in memory: In-memory state is lost on hibernation or restart. Persist anything that cannot be reconstructed from clients.
- Creating rooms with user IDs instead of document IDs: Rooms should map to collaborative contexts, not individual users. A room per user defeats the purpose.
- Broadcasting raw client messages without validation: A malicious client can send anything. Always parse and validate before broadcasting or storing.
When to Use
- Collaborative editors and whiteboards needing a stateful server per document with persistence.
- Game lobbies and matchmaking where each lobby is an isolated stateful room.
- AI chat interfaces where the server maintains conversation state and streams responses.
- Presence-heavy applications that need edge-deployed low-latency WebSocket handling.
- Prototyping real-time features with zero infrastructure setup -- deploy with
npx partykit deploy.
Install this skill directly: skilldb add realtime-services-skills
Related Skills
Ably Realtime
Ably is a robust, globally distributed real-time platform offering publish/subscribe messaging, presence, and channels.
Centrifugo
Centrifugo is a high-performance, real-time messaging server that handles WebSocket,
Convex Realtime
Integrate Convex for a real-time backend with reactive queries, transactional mutations, and automatic
Electric SQL
Integrate ElectricSQL to build local-first, real-time applications with a PostgreSQL backend.
Firebase Realtime Db
Integrate Firebase Realtime Database for synchronized data with listeners, offline persistence,
Liveblocks
Integrate Liveblocks for collaborative features including real-time presence, conflict-free storage,