Skip to main content
Technology & EngineeringRealtime Services240 lines

Partykit

Integrate PartyKit for serverless real-time applications using WebSocket rooms backed by Durable Objects. Build stateful collaboration servers with zero infrastructure management and edge-deployed compute.

Quick Summary27 lines
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: 240 lines
Paste into your CLAUDE.md or agent config

PartyKit 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

Get CLI access →

Related Skills

Phoenix Channels

Integrate Phoenix Channels for robust, real-time communication in web applications. Leverage Elixir's concurrency and fault tolerance to build scalable pub/sub systems, enabling features like live chat, notifications, and collaborative editing with low latency and high reliability.

Realtime Services298L

Socket Io

Integrate Socket.IO for real-time bidirectional event-based communication between clients and servers. Handle namespaces, rooms, acknowledgements, middleware, and binary streaming with proper error recovery.

Realtime Services219L

SSE

Implement Server-Sent Events for unidirectional server-to-client streaming over HTTP. Handle EventSource connections, custom event types, automatic reconnection, and streaming responses for AI and live feeds.

Realtime Services263L

Supabase Realtime

Integrate Supabase Realtime for listening to Postgres changes, broadcasting messages between clients, and tracking user presence. Configure channels, row-level security, and subscription lifecycles correctly.

Realtime Services213L

Websocket Native

Implement native WebSocket connections for full-duplex communication. Handle connection lifecycle, binary data, heartbeats, reconnection strategies, and message framing without third-party libraries.

Realtime Services332L

Yjs CRDT

Yjs is a high-performance, open-source CRDT implementation for building robust collaborative applications. It enables real-time synchronization of shared data structures like text, arrays, and maps across multiple clients without conflicts. Choose Yjs when you need strong offline-first capabilities and complex collaborative editing experiences for web applications.

Realtime Services231L