TypeSafe Primitives for a Realtime Web
Open-source, multiplayer APIs powered-by TypeScript inference end-to-end
// backend const sendMessage = io.procedure .input(z.object({ message: z.string() })) .broadcast(({ message }) => ({ receiveMessage: { message }, })); const router = io.router({ sendMessage }); const ioServer = io.server({ router }); // frontend event.receiveMessage.useEvent(({ data }) => { console.log(data.message); }); broadcast.sendMessage({ message: "Hello!" });
Automatic type-safety
Leverage the full-power of TypeScript interfence to catch errors quickly and get IDE auto-completions as you're developing!
// pnpm install @pluv/platform-node createIO(platformNode()); // pnpm install @pluv/platform-cloudflare createIO(platformCloudflare());
const GET = async (req: Request) => { const room = getRoomFromUrl(req.url); const { user } = await getSession(req); const token = await ioServer .createToken({ room, user }); return new Response(token, { status: 200 }); };
const { useBroadcast, useOthers, // ... } = createBundle(client); const broadcast = useBroadcast(); const names = useOthers((others) => { return others.map(({ user }) => user.name); });
// pnpm install @pluv/platform-pluv const io = createIO( platformPluv({ authorize: { user: z.object({ id: z.string() }), }, basePath: "/api/pluv", publicKey: "pk_...", secretKey: "sk_...", webhookSecret: "whsec_...", }) );
Usage monitoring
When hosted on the pluv.io network, track active rooms and connections, and custom events to gain insights on how your app is being used.
const client = createClient({ types, initialStorage: yjs.doc(() => ({ messages: yjs.array<string>([]), })), }); const [messages, type] = useStorage("messages"); const addMessage = (message: string) => { type?.push([message]); }; messages?.map((message, i) => ( <div key={i}>{message}</div> ));
const client = createClient({ types, presence: z.object({ cursor: z.object({ x: z.number(), y: z.number(), }).nullable(), }), }); const [myCursor] = useMyPresence( ({ cursor }) => cursor, ); const cursors = useOthers((others) => ( others.map(({ presence }) => presence.cursor) ));
// backend const sendMessage = io.procedure .input(z.object({ message: z.string() })) .broadcast(({ message }) => ({ receiveMessage: { message }, })); const router = io.router({ sendMessage }); const ioServer = io.server({ router }); // frontend event.receiveMessage.useEvent(({ data }) => { console.log(data.message); }); broadcast.sendMessage({ message: "Hello!" });
Automatic type-safety
Leverage the full-power of TypeScript interfence to catch errors quickly and get IDE auto-completions as you're developing!
const client = createClient({ types, initialStorage: yjs.doc(() => ({ messages: yjs.array<string>([]), })), }); const [messages, type] = useStorage("messages"); const addMessage = (message: string) => { type?.push([message]); }; messages?.map((message, i) => ( <div key={i}>{message}</div> ));
// pnpm install @pluv/platform-pluv const io = createIO( platformPluv({ authorize: { user: z.object({ id: z.string() }), }, basePath: "/api/pluv", publicKey: "pk_...", secretKey: "sk_...", webhookSecret: "whsec_...", }) );
const GET = async (req: Request) => { const room = getRoomFromUrl(req.url); const { user } = await getSession(req); const token = await ioServer .createToken({ room, user }); return new Response(token, { status: 200 }); };
const { useBroadcast, useOthers, // ... } = createBundle(client); const broadcast = useBroadcast(); const names = useOthers((others) => { return others.map(({ user }) => user.name); });
// pnpm install @pluv/platform-node createIO(platformNode()); // pnpm install @pluv/platform-cloudflare createIO(platformCloudflare());
const client = createClient({ types, presence: z.object({ cursor: z.object({ x: z.number(), y: z.number(), }).nullable(), }), }); const [myCursor] = useMyPresence( ({ cursor }) => cursor, ); const cursors = useOthers((others) => ( others.map(({ presence }) => presence.cursor) ));
Usage monitoring
When hosted on the pluv.io network, track active rooms and connections, and custom events to gain insights on how your app is being used.
Automatic type-safety
Leverage the full-power of TypeScript interfence to catch errors quickly and get IDE auto-completions as you're developing!
Usage monitoring
When hosted on the pluv.io network, track active rooms and connections, and custom events to gain insights on how your app is being used.
Native-like Realtime Data
Treat your realtime data like ordinary state.Storage data is shared and modifiable by all participants.
Developer-Focused APIs
Unlock powerful utilities to build complex multiplayer experiences in minutes, not days.
Create your PluvIO server
To get started with pluv.io, you will first need to create a PluvIO
server that can start registering new realtime connections.
Pick which platform (i.e. adapter) you wish to connect to. Connect to pluv.io as a fully-managed service for your Next.js app. Or self-host on Node.js or on Cloudflare Workers.
Define a Zod schema validator to ensure the structure and type-safety of authorized users in your rooms.
const io = createIO( platformPluv({ authorize: { user: z.object({ id: z.string(), image: z.string().nullable(), name: z.string(), }), }, basePath: "/api/pluv", crdt: yjs, publicKey: "pk_...", secretKey: "sk_...", webhookSecret: "whsec_...", }), ); export const ioServer = io.server();
Set-up HTTP endpoints
Next, set-up custom authorization for your rooms. And mount the `PluvServer` to enable realtime connections to your pluv.io instance.
Setup will vary depending on the platform (e.g. Node.js or Cloudflare Workers) used.
// /api/auth/room/route.ts export const GET = async (req: Request) => { const room = getRoomFromUrl(req.url); const { user } = await getSession(req); const token = await ioServer.createToken({ room, user }); return new Response(token, { status: 200 }); }; // /api/pluv/route.ts export const POST = ioServer.fetch;
Prepare your frontend bundle
ioServer
. This frontend bundle contains all of pluv.io's APIs for realtime collaboration.You can optionally unlock more realtime capabilities for your app by defining:
- A presence state for each user with Zod.
- CRDT storage with Yjs or Loro.
- Custom typesafe events on your backend[1] or your frontend.
const types = infer((i) => ({ io: i<typeof ioServer> })); const io = createClient({ types, authEndpoint: ({ room }) => `/api/auth/room?${room}`, initialStorage: yjs.doc(() => ({ tasks: yjs.array([ { id: "TASK-4753", status: "todo" }, { id: "TASK-2676", status: "progress" }, ]), })), presence: z.object({ selectedId: z.string().nullable(), }), }); const router = io.router({ sendGreeting: io.procedure .input(z.object({ greeting: z.string() })) .broadcast(({ greeting }) => ({ receiveGreeting: { greeting }, })), }); export const { PluvRoomProvider, event, useBroadcast, useMyPresence, useStorage, } = createBundle(io, { router });
Wrap with PluvRoomProvider
The room bundle provides a PluvRoomProvider
to wrap your page with. Once you do, pluv.io APIs can now be used in nested components!
const Page: FC<Props> = ({ children, roomId }) => ( <PluvRoomProvider initialPresence={{ selectedId: null }} room={roomId} > {children} </PluvRoomProvider> );
Build with typesafe realtime primitives!
With our frontend bundle ready to use, you can start using pluv.io realtime primitives with TypeScript autocompletion and intellisense for your custom events, presence and storage.
Type definitions will be as narrow as you've configured, with minimal manual type definitions and without code-generation!
const io = createIO( platformPluv({ authorize: { user: z.object({ id: z.string(), image: z.string().nullable(), name: z.string(), }), }, basePath: "/api/pluv", crdt: yjs, publicKey: "pk_...", secretKey: "sk_...", webhookSecret: "whsec_...", }), ); export const ioServer = io.server();