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.
Every Server instance runs inside a Durable Object that comes with its own isolated, transactional storage. That storage is co-located with the DO’s compute, making reads and writes fast without any network round-trip. PartyServer exposes it through this.ctx.storage, the standard Cloudflare Transactional Storage API, plus a SQLite interface for structured data and alarm scheduling for deferred work.
Key-value storage via ctx.storage
The ctx.storage object supports get, put, delete, list, and transactional variants (getAlarm, setAlarm, deleteAlarm, transaction, transactionSync). Values can be any serializable type; the maximum value size is 128 KiB.
Loading state in onStart
The best time to read from storage is in onStart, which runs once when the DO boots or wakes from hibernation. Because onStart executes inside ctx.blockConcurrencyWhile, no connections are accepted and no messages are processed until it resolves. This gives you a safe, synchronized window to hydrate in-memory state before any client code runs.
Avoid reading from ctx.storage on every incoming message. Load what you
need once in onStart and keep it in memory; write back to storage only
when the value actually changes.
Example: a persistent counter
import { Server } from "partyserver";
import type { Connection, WSMessage } from "partyserver";
export class CounterServer extends Server {
count = 0;
// Load the counter once on start
async onStart() {
this.count = (await this.ctx.storage.get<number>("count")) ?? 0;
}
onConnect(connection: Connection) {
// Send the current count to newly connected clients
connection.send(JSON.stringify({ type: "count", value: this.count }));
}
async onMessage(connection: Connection, message: WSMessage) {
const { type } = JSON.parse(message as string) as { type: string };
if (type === "increment") {
this.count += 1;
await this.ctx.storage.put("count", this.count);
// Broadcast new value to all connections
this.broadcast(JSON.stringify({ type: "count", value: this.count }));
}
}
}
SQLite-backed storage via ctx.storage.sql
Each Durable Object that uses new_sqlite_classes in wrangler.jsonc has its own embedded SQLite database. Access it through this.ctx.storage.sql.exec:
async onStart() {
// Create a table if it doesn't already exist
this.ctx.storage.sql.exec(`
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user TEXT NOT NULL,
body TEXT NOT NULL,
ts INTEGER NOT NULL DEFAULT (unixepoch())
)
`);
}
async onMessage(connection: Connection, message: WSMessage) {
const { user, body } = JSON.parse(message as string) as {
user: string;
body: string;
};
// Persist the message
this.ctx.storage.sql.exec(
"INSERT INTO messages (user, body) VALUES (?, ?)",
user,
body
);
// Broadcast to all clients
this.broadcast(message);
}
PartyServer also ships a tagged-template helper on the Server class so you can write type-safe queries with interpolated parameters:
type MessageRow = { id: number; user: string; body: string; ts: number };
const recent = this.sql<MessageRow>`
SELECT * FROM messages ORDER BY ts DESC LIMIT 50
`;
// recent is MessageRow[]
Declare your DO class under new_sqlite_classes (not new_classes) in
wrangler.jsonc to enable the SQLite API. Classes registered under
new_classes use legacy key-value storage only.
Alarms: scheduling deferred work
ctx.storage.setAlarm(time) schedules a future call to onAlarm. Only one alarm can be pending per DO instance at a time, but you can reschedule inside onAlarm to implement recurring work.
import { Server } from "partyserver";
export class HeartbeatServer extends Server {
async onStart() {
// Schedule the first heartbeat 30 seconds from now
await this.ctx.storage.setAlarm(Date.now() + 30_000);
}
async onAlarm() {
// Broadcast a heartbeat to every connected client
this.broadcast(JSON.stringify({ type: "heartbeat", ts: Date.now() }));
// Reschedule for the next interval
await this.ctx.storage.setAlarm(Date.now() + 30_000);
}
}
alarm() is the only entry point that can run after a DO has been evicted.
This makes it the canonical way to perform cleanup or periodic maintenance
even when no clients are connected.
Alarm-based session expiry
const SESSION_TTL_MS = 60 * 60 * 1000; // 1 hour
export class SessionServer extends Server {
async onConnect(connection) {
// Reset the session expiry on every new connection
await this.ctx.storage.setAlarm(Date.now() + SESSION_TTL_MS);
}
async onAlarm() {
// Archive state before the DO is evicted
const history = await this.ctx.storage.get<string[]>("history");
await this.ctx.storage.put("archived", history);
await this.ctx.storage.delete("history");
// Notify any lingering connections
this.broadcast(JSON.stringify({ type: "session-expired" }));
}
}
Using environment bindings
this.env gives you access to any bindings declared in wrangler.jsonc — KV namespaces, R2 buckets, D1 databases, AI, service bindings, and secrets. These are useful when you need to store data that outlives an individual DO instance or that must be shared across instances:
export class PresenceServer extends Server {
async onConnect(connection) {
// Write presence to a shared KV namespace
await this.env.PRESENCE_KV.put(
`user:${connection.id}`,
JSON.stringify({ server: this.name, ts: Date.now() }),
{ expirationTtl: 300 }
);
}
async onClose(connection) {
await this.env.PRESENCE_KV.delete(`user:${connection.id}`);
}
}
Declare the binding in wrangler.jsonc:
{
"kv_namespaces": [
{ "binding": "PRESENCE_KV", "id": "<your-kv-id>" }
]
}