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

TypeSafe Primitivesfor a Realtime Web

Open Source, multiplayer APIs powered-by TypeScript inference end-to-end.

Welcome back!

Experiences like these should be collaborative. Click around!

No other users online
No results.
Table adapted from shadcn/ui

Automatic Type-safety

Get auto-completion and in-code errors with end-to-end type-safety.


Build for either Cloudflare Workers or Node.js runtimes.


Edit shared data and documents with the Yjs or Loro ecosystems.


Have users directly interact with eachother in realtime with per-user states.

Authentication & Identity

Give each users their own identity with custom authentication rules.

No Vendor Lock

Pluv is designed for self-hosting first with documented instructions here and here.

Developer-Focused APIs

Unlock powerful utilities to make building complex multiplayer experiences easier.


Create your PluvIO server

To get started with, you will first need to create a PluvIO server that can start registering new websocket connections.

On the server, you can define any number of event procedures that your frontend can broadcast to other connections in the same realtime room.

In these event procedures, you can optionally define Zod schema validators to ensure that the inputs are defined in the same way our backend expects when they are received.

Lastly, define a type export of the IOServer for our frontend to use.

const io = createIO({ platform: platformNode() });

const router = io.router({
  sendGreeting: io.procedure
    .input(z.object({ name: z.ZodStringname: z.string() }))
name: string
}) => ({
receiveGreeting: { greeting: `Hi! I'm ${name: stringname}!` }, })), }); export const ioServer = io.server({ router }); export type IOServer = typeof ioServer;

Set-up your HTTP and Websocket servers

Next, set-up our HTTP and WebSocket servers using our ioServer.

Set-up may vary between Node.js and Cloudflare Worker runtimes.

const app = express();
const server = Http.createServer(app);
const Pluv = createPluvHandler({ io: ioServer, server });



Prepare your frontend bundle

Afterwards, we will prepare our frontend bundle by using a type import of our IOServer. This frontend bundle contains all of's APIs for realtime collaboration.

You can optionally define a presence for each user with Zod, and CRDT storage with Yjs or Loro to unlock more realtime capabilities for your app.

const client = createClient<IOServer>({
  wsEndpoint: (room) => `wss://${room}`,

const { createRoomBundle } = createBundle(client);

export const pluv = createRoomBundle({
  presence: z.object({
    selectionId: z.string().nullable(),
  initialStorage: yjs.doc(() => ({
    tasks: yjs.array([
      { id: "TASK-4753", status: "todo", priority: "medium" },
      { id: "TASK-2676", status: "progress", priority: "high" },
      { id: "TASK-5720", status: "progress", priority: "high" },

Wrap with PluvRoomProvider

The room bundle provides a PluvRoomProvider to wrap your page with. Once you do, your app is now multiplayer with!

const Page: React.FC<PageProps> = ({ children, roomId: stringroomId }) => (
initialPresence: {
    selectionId: string | null;
={{ selectionId: null }}
room: stringroom={roomId: stringroomId} > {children} </pluv.PluvRoomProvider> );

Start building with realtime primitives!

With our frontend bundle ready to use, you can start using realtime primitives with TypeScript autocompletion and intellisense matching your backend events, presence and storage.

Type definitions will be as narrow as you've configured, all while managing minimal TypeScript type definitions and without code-generation!

const broadcast = pluv.useBroadcast();

sendGreeting: (input: {
    name: string;
}) => void
({ name: "leedavidcs" });
pluv.event.receiveGreeting.useEvent(({ data }) => { console.log(data.
greeting: string
}); const [
const selectionId: string | null
] = pluv.useMyPresence((presence) => {
return presence.selectionId; }); const [
const tasks: {
    id: string;
    status: string;
    priority: string;
}[] | null
] = pluv.useStorage("tasks");

Native-like Realtime Data

Code as-if you're directly working with realtime data as any other data, as if it were a native frontend concept.

User 1
Drag the boxes
User 2
Drag the boxes