🎉 pluv.io is now stable! Read our announcement here

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
Call and listen to events. Interact with shared storageClientnpm install @pluv/client
React-bindings for @pluv/clientClientnpm install @pluv/react
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:

// server/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 });
});

Create the client bundle

// frontend/pluv.ts

"use client";

import { createClient } from "@pluv/client";
import { yjs } from "@pluv/crdt-yjs";
import { createBundle, infer } from "@pluv/react";

// Use a type-import, since this will be used on the frontend
import type { ioServer } from "./server/pluv";

// Create `types` outside of `createClient` due to TypeScript inference
// limitations
const types = infer((i) => ({ io: i<typeof ioServer> }));

// Note that the `wsEndpoint` param is omitted when using
// `@pluv/platform-pluv`
export const client = createClient({
    types,
    // Wherever your auth endpoint was
    authEndpoint: ({ room }) => `/api/pluv/auth?room=${room}`,
    wsEndpoint: ({ room }) => {
        const host = document.location.host;
        const protocol = document.location.protocol;
        const wsProtocol = protocol.includes("https") ? "wss:" : "ws:";

        // Or wherever you intend to receive the connection requests from
        return `${wsProtocol}//${host}/api/pluv/room?room=${room}`;
    },
});

// Destructure the bundle, as the app router will not allow accessing
// properties of objects within server components (e.g. you are unable to
// write pluv.PluvRoomProvider in a layout that is a server component)
export const {
    PluvRoomProvider,

    useStorage,
    useMyself,
    useOthers,
    useRoom,
    useTransact,

    // ... Other values as needed

    // Alternatively for things that will only be used in client components
    // (i.e. not server components)
    ...pluv,
} = createBundle(client);

Wrap the PluvRoomProvider

At the route the room's layout will be defined, wrap the page with the PluvRoomProvider that was destructured from the previous step. This is necessary to enable all other functions from createBundle.

import type { FC, ReactNode } from "react";

interface PageProps {
    children?: ReactNode;
    params: Promise<{ room: string }>;
}

const Page: FC<PageProps> = async (props) => {
    const { room } = await params;

    return (
        <PluvRoomProvider room={room}>
            {children}
        </PluvRoomProvider>
    );
};

export default Page;

And that's it! With this, functions (e.g. hooks) from createBundle can be used for components within your provider.