Cloudflare Durable Objects Example
This example demonstrates building a real-time WebSocket server using Cloudflare Durable Objects with a Vite frontend client.Features
- Durable Objects: Stateful WebSocket server with SQLite storage
- WebSocket: Real-time bidirectional communication
- Vite Client: Modern frontend with WebSocket client
- Type Safety: Full TypeScript types for Durable Objects
Project Setup
import alchemy from "alchemy";
import { DurableObjectNamespace, Vite, Worker } from "alchemy/cloudflare";
import type { WebSocketServer } from "./src/server.ts";
const app = await alchemy("cloudflare-durable-object-websocket");
export const server = await Worker("server", {
name: `${app.name}-${app.stage}-server`,
entrypoint: "src/server.ts",
adopt: true,
bindings: {
WS_SERVER: DurableObjectNamespace<WebSocketServer>("ws-server", {
className: "WebSocketServer",
sqlite: true,
}),
},
dev: {
tunnel: !process.env.ALCHEMY_E2E,
},
});
console.log("Server:", server.url);
export const client = await Vite("client", {
name: `${app.name}-${app.stage}-client`,
adopt: true,
env: {
VITE_WEBSOCKET_URL: server.url!,
},
dev: {
command: "vite dev --port 5001",
},
});
console.log("Client:", client.url);
if (process.env.ALCHEMY_E2E) {
const { test } = await import("./test/e2e.ts");
await test({
url: server.url,
});
}
await app.finalize();
import { DurableObject } from "cloudflare:workers";
import type { server } from "../alchemy.run.ts";
export default {
fetch: (req: Request, env: typeof server.Env) => {
const url = new URL(req.url);
if (url.pathname === "/status") {
return new Response("OK");
}
if (url.pathname !== "/websocket") {
return new Response("Not found", { status: 404 });
}
const stub = env.WS_SERVER.getByName("default");
return stub.fetch(req);
},
};
export class WebSocketServer extends DurableObject {
declare env: typeof server.Env;
async fetch(req: Request): Promise<Response> {
if (req.headers.get("upgrade") !== "websocket") {
return new Response("Not a websocket request", { status: 400 });
}
const wsPair = new WebSocketPair();
const { 0: client, 1: server } = wsPair;
this.ctx.acceptWebSocket(server);
console.log("accepting websocket request");
return new Response(null, {
status: 101,
webSocket: client,
});
}
webSocketMessage(
ws: WebSocket,
message: string | ArrayBuffer,
): void | Promise<void> {
console.log("message", message);
ws.send(`Received message: ${message.toString()}`);
}
webSocketClose(
_ws: WebSocket,
code: number,
reason: string,
): void | Promise<void> {
console.log("close", code, reason);
}
webSocketError(_ws: WebSocket, error: unknown): void | Promise<void> {
console.log("error", error);
}
}
const ws = new WebSocket(
import.meta.env.VITE_WEBSOCKET_URL.replace('http', 'ws') + '/websocket'
);
ws.addEventListener('open', () => {
console.log('Connected to WebSocket server');
ws.send('Hello from client!');
});
ws.addEventListener('message', (event) => {
console.log('Received:', event.data);
});
Key Features Explained
Durable Object with SQLite
Durable Objects support SQLite storage for persistence:WebSocket Lifecycle
Durable Objects provide WebSocket lifecycle hooks:webSocketMessage: Handle incoming messageswebSocketClose: Handle connection closewebSocketError: Handle errors