pluv.io is in preview! Please wait for a v1.0.0 stable release before using this in production.

Cloudflare Workers

pluv.io supports building real-time APIs with Cloudflare Workers through their Durable Objects API. You can define your handler and your DurableObject manually if you need more control, but if you'd like to get started quickly, check out createPluvHandler.

Using with Cloudflare Workers (manual)

Let's step through how we'd put together a real-time API for Cloudflare Workers. The examples below assumes a basic understanding of Cloudflare Workers and Durable Objects.

Install dependencies

1# For the server
2npm install @pluv/io @pluv/platform-cloudflare
3
4# Server peer-dependencies
5npm install zod

Create PluvIO instance

Define an io (websocket client) instance on the server codebase:

1// server/io.ts
2
3import { createIO } from "@pluv/io";
4import { platformCloudflare } from "@pluv/platform-cloudflare";
5import { eq } from "drizzle-orm";
6import { drizzle } from "drizzle-orm/d1";
7import { schema } from "./schema";
8
9export type Env = {
10 DB: D1Database;
11};
12
13export const io = createIO({
14 platform: platformCloudflare<Env>(),
15 // Example of using Cloudflare's D1 database with drizzle-orm
16 getInitialStorage: async ({ env, room }) => {
17 const db = drizzle(env.DB, { schema });
18
19 const existingRoom = await db
20 .select({ encodedState: schema.rooms.encodedState })
21 .from(schema.rooms)
22 .where(eq(schema.rooms.name, room))
23 .get();
24
25 return existingRoom?.encodedState ?? null;
26 },
27 onRoomDeleted: async ({ encodedState, env, room }) => {
28 const db = drizzle(env.DB, { schema });
29
30 await db
31 .insert(schema.rooms)
32 .values({
33 name: room,
34 encodedState,
35 })
36 .onConflictDoUpdate({
37 target: schema.rooms.name,
38 set: { encodedState },
39 })
40 .run();
41 },
42});
43
44// Export the websocket client io type, instead of the client itself
45export type AppPluvIO = typeof io;

Attach to a RoomDurableObject

Next, create a RoomDurableObject and attach our new pluv.io instance to the room:

1// server/RoomDurableObject.ts
2
3import { type InferIORoom } from "@pluv/io";
4import { AppPluvIO, Env, io } from "./io";
5
6export class RoomDurableObject implements DurableObject {
7 private _env: Env;
8 private _io: InferIORoom<AppPluvIO>;
9
10 constructor(state: DurableObjectState, env: Env) {
11 this._env = env;
12 this._io = io.getRoom(state.id.toString(), { env });
13 }
14
15 async fetch(request: Request) {
16 if (request.headers.get("Upgrade") !== "WebSocket") {
17 return new Response("Expected WebSocket", { status: 400 });
18 }
19
20 const { 0: client, 1: server } = new WebSocketPair();
21
22 await this._io.register(server, {
23 env: this._env,
24 request,
25 });
26
27 return new Response(null, { status: 101, webSocket: client });
28 }
29}

Forward request to RoomDurableObject

Lastly, integrate your RoomDurableObject with your Cloudflare Worker's default handler:

1// server/index.ts
2
3const parseRoomId = (url: string): string => {
4 /* get room from req.url */
5};
6
7const handler = {
8 async fetch(req: Request, env: Env): Promise<Response> {
9 const roomId = parseRoomId(req.url);
10 // In wrangler.toml:
11 // [durable_objects]
12 // bindings = [{ name = "rooms", class_name = "RoomDurableObject" }]
13 const durableObjectId = env.rooms.idFromString(roomId);
14
15 const room = env.rooms.get(durableObjectId);
16
17 return room.fetch(request);
18 },
19};
20
21export default handler;

createPluvHandler

If you don't need to modify your DurableObject or Cloudflare Worker handler too specifically, @pluv/platform-cloudflare also provides a function createPluvHandler to create a DurableObject and handler for you automatically.

1import { createIO } from "@pluv/io";
2import { createPluvHandler, platformCloudflare } from "@pluv/platform-cloudflare";
3
4const io = createIO({
5 platform: platformCloudflare(),
6});
7
8const Pluv = createPluvHandler({
9 // Your PluvIO instance
10 io,
11 // Your durable object binding, defined in wrangler.toml
12 binding: "rooms",
13 // Optional: Specify the base path from which endpoints are defined
14 endpoint: "/api/pluv", // default
15 // If your PluvIO instance defines authorization, add your authorization
16 // logic here. Return a user if authorized, return null or throw an error
17 // if not authorized.
18 authorize(request: Request, roomId: string): Promise<User> {
19 return {
20 id: "abc123",
21 name: "leedavidcs"
22 };
23 },
24 // Optional: If you want to modify your response before it is returned
25 modify: (request, response) => {
26 if (request.headers.get("Upgrade") === "websocket") return response;
27
28 // Add custom headers if you want
29 response.headers.append("access-control-allow-origin", "*");
30
31 return response;
32 },
33});
34
35// Export your Cloudflare Worker DurableObject with your own custom name
36// Then in wrangler.toml:
37// [durable_objects]
38// bindings = [{ name = "rooms", class_name = "RoomDurableObject" }]
39export const RoomDurableObject = Pluv.DurableObject;
40
41// Export your Cloudflare Worker handler
42export default Pluv.handler;
43
44// Alternatively, define your own custom handler
45export default {
46 async fetch(request: Request, env: Env): Promise<Response> {
47 const response = await Pluv.fetch(request, env);
48
49 // matched with the Pluv handler, return response
50 if (response) return response;
51
52 // didn't match with Pluv handler, add your own worker logic
53 // ...
54
55 return new Response("Not found", { status: 404 });
56 }
57};