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:

PurposeLocationInstall command
Register websockets and custom eventsServernpm install @pluv/io
Adapter for Node.js runtimeServernpm install @pluv/platform-node
yjs CRDTBothnpm 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 });
});