Node.js
@pluv/io
is designed to be self-hosted on the Node.js runtime. Let's step through how we'd put together a real-time API for Node.js.
Installation
To install pluv.io for Node.js (self-hosted), we will install the following packages from pluv.io:
Purpose | Location | Install command |
---|---|---|
Register websockets and custom events | Server | npm install @pluv/io |
Adapter for Node.js runtime | Server | npm install @pluv/platform-node |
yjs CRDT | Both | npm install @pluv/crdt-yjs |
Installation Example
Here is an example installation for npm for you to copy:
# For the server
npm install @pluv/io @pluv/platform-node
# Server peer dependencies
npm install ws zod
# Optional if you wish to use CRDT storage
# For storage capabilities
npm install @pluv/crdt-yjs
# Storage peer dependencies
npm install yjs
Create PluvIO instance
Define an io (websocket client) instance on the server codebase:
// backend/io.ts
import { yjs } from "@pluv/crdt-yjs";
import { createIO } from "@pluv/io";
import { platformNode } from "@pluv/platform-node";
import { z } from "zod";
import { Database } from "./db";
export const io = createIO(
platformNode({
// Optional: Authorization is optional for `@pluv/platform-node`
// If excluded, your `user` will be `null` on the frontend.
authorize: {
// Your own secret for generating JWTs
secret: process.env.PLUV_AUTH_SECRET!,
// The shape of your user object. `id` must be provided
user: z.object({
id: z.string(),
// Here is an additional field we can add
name: z.string(),
}),
},
// Optional: Context that will be made available in event
// handlers and procedures
context: () => ({
db: new Database(process.env.DATABASE_URL),
}),
// Optional: Only if you want to use CRDT storage features
crdt: yjs,
// Optional: If you want to enable development logging
debug: process.env.NODE_ENV === "development",
})
);
export const ioServer = io.server({
// Optional: Only if you're using storage, and persisting
// to a database
getInitialStorage: async ({ room, context }) => {
const { db } = context;
// Stubbed example DB query
const rows = await db.sql(
"SELECT storage FROM room WHERE name = ?;",
[room]
);
const storage = rows[0]?.storage;
return storage;
},
// Optional: Only if you want to run code when a room
// is deleted, such as saving the last storage state
onRoomDeleted: async ({ room, encodedState, context }) => {
// Upsert the db room with last storage state
},
});
Integrate PluvIO with ws
Integrate with ws on your Node.js server.
// backend/server.ts
import type { InferIORoom } from "@pluv/io";
import { serve, type HttpBindings } from "@hono/node-server";
import Http from "http";
import { Hono } from "hono";
import Ws from "ws";
import { ioServer } from "./io";
const PORT = 3000;
// Stub example to get roomId from url
const parseRoomId = (url: string): string => {
/* get room from req.url */
};
const app = new Hono<{ Bindings: HttpBindings }>()
// Only if you specified `authorize` on @pluv/io
.get("/api/pluv/auth", async (c) => {
const room = c.req.query("room") as string;
const req: Http.IncomingMessage = c.env.incoming;
// Example stub. However you get the authed user here
const user = await getUser(req);
const token = await ioServer.createToken({ user, req, room });
return c.text(token, 200);
});
const server = serve({ fetch: app.fetch, port: PORT }) as Http.Server;
const wsServer = new Ws.WebSocketServer({ server });
// Manage rooms in-memory
const rooms = new Map<string, InferIORoom<typeof ioServer>>();
const getRoom = (roomId: string): InferIORoom<typeof ioServer> => {
const existing = rooms.get(roomId);
if (existing) return existing;
const newRoom = ioServer.createRoom(roomId, {
onDestroy: (event) => {
rooms.delete(event.room);
},
});
rooms.set(roomId, newRoom);
return newRoom;
};
// Setup your websocket request handler
wsServer.on("connection", async (ws, req) => {
const roomId = parseRoomId(req.url);
const room = getRoom(roomId);
// Token is only needed if you have configured authorization
const token = new URL(req.url).searchParams.get("token");
await room.register(ws, { req, token });
});