Introduction
Building multiplayer applications is hard. There are countless challenges developers need to “get right” in order to enable a functioning multiplayer experience for their users including but not limited to:
- Heartbeats
- Reconnects
- Rooms
- Synchronizing data
- Presence
And when solving for these, significant complexity is often introduced to the application that inhibits shipping value to end users.
Thankfully MIT licensed OSS has continued to evolve quickly in solving many of these challenges. Tools such as socket.io and ws have simplified heartbeats, reconnects and rooms (including additional considerations like multiplexing and multi-servers); and in more recent years, partykit, y-sweet and hocuspocus have done more to also enable presence and synchronizing data via CRDTs like Yjs. But even so, we can go even further with the developer experience 🤔.
Speaking with engineers and reading many codebases and discourses, I’ve discovered that so many developers often really struggle to manage TypeScript types but continue to do so because they value the static analysis and IDE intellisense so highly. As a result, we’ve seen approximately 3 years ago, tRPC quickly gained enormous popularity as developers enjoyed the end-to-end type safety it enabled while also not having to manage types manually. And with tools such as the TanStack suite and Drizzle ORM this trend of maximizing type-inference seems to be continuing when looking at the resources developers continue to reach for when building applications over recent years.
3 years ago, while working at Openbase (YC S20), our data suggested Yjs to be one of most popular and best maintained OSS packages for the realtime category at the time. Meanwhile, I was getting increasingly interested in the Cloudflare Worker platform as it was maturing, particularly in what Durable Objects enabled. So I decided then: “let’s try building tRPC for realtime” (before subscription support eventually landed) and created the following criteria:
- Maximize type-inference (i.e. little to no managing of TypeScript types)
- Yjs as a first-class citizen
- Frontend framework agnostic, yet first-class support for them (like that of TanStack Table)
- Easy to self-host on either the Node.js or Cloudflare Worker runtimes
- Provide primitives for developers to create libraries or share code recipes with others
Today, adhering to these goals, pluv.io is launching v1.0.0 as a new entry into the realtime OSS landscape. With this v1 release, pluv.io will now follow semantic versioning with more comprehensive release notes for future changes to the library.
Examples
Getting started with pluv.io aligns fairly closely with doing the same for trpc.
Here we create a pluv.io custom event called
sendGreeting
that takes an object with a string message
as an input and returns an object containing receiveGreeting
.// example-backend import { createIO } from "@pluv/io"; import { platformCloudflare } from "@pluv/platform-cloudflare"; import { z } from "zod"; const io = createIO(platformCloudflare()); const router = io.router({ sendGreeting: io.procedure .input(z.object({ message: z.string() }) .broadcast(({ message }) => ({ receiveGreeting: { greeting: message }, })), }); export const ioServer = io.server({ router });
Then we create a type safe client by connecting our server via a TypeScript type-only import.
// example-frontend import { createClient, infer } from "@pluv/client"; import type { ioServer } from "example-backend"; const types = infer((i) => ({ io: i<typeof ioServer> })); export const io = createClient({ types }); const room = io.createRoom("my-example-room"); room.broadcast.sendGreeting({ message: "hello world" }); // ^? const sendGreeting: (data: { message: string }) => void const unsubscribe = room.event.receiveGreeting( ({ data }) => { /* ... */ } // ^? const data: { greeting: string } );
What about handling presence?
We use zod to create a presence schema to then unlock presence APIs.
import { z } from "zod"; export const io = createClient({ // ... presence: z.object({ selectionId: z.string().nullable(), }), }); const room = io.createRoom("my-example-room", { // ... initialPresence: { selectionId: null }, }); room.updateMyPresence({ selectionId: "id_..." }); const unsubscribe = room.subscribe( "others", (others) => { /* ... */ }, // ^? const others: readonly { // connectionId: string; // presence: { selectionId: string | null }; // }[], );
What about handling synchronized data?
We use @pluv/crdt-yjs to enable synchronized data storage via Yjs.
import { yjs } from "@pluv/crdt-yjs"; import type { Array as YArray } from "yjs"; // backend const io = createIO( platformCloudflare({ // ... crdt: yjs, }), ); // frontend export const io = createClient({ // ... initialStorage: yjs.doc(() => ({ messages: yjs.array<string>([]), })), }); const room = io.createRoom({ /* ... */ }); const sharedType = room.getStorage("messages"); // ^? const sharedType: YArray<string>; const unsubscribe = room.storage( "messages", (messages: string[]) => { /* ... */ } );
And then more…
A more comprehensive feature list and roadmap can be seen in our introduction docs here.
To give you a small glimpse, remember criteria #3 (regarding first-class frontend framework support)?
import { createBundle } from "@pluv/react"; const io = createClient({ /* ... */ }); const { // components MockedRoomProvider, PluvProvider, PluvRoomProvider, // utils event, // hooks useBroadcast, useCanRedo, useCanUndo, useClient, useConnection, useDoc, useEvent, useMyPresence, useMyself, useOther, useOthers, useRedo, useRoom, useStorage, useTransact, useUndo, } = createBundle(io);
Get started today!
pluv.io does offer a fully-managed service for those who want to connect their apps to the thing that just works (e.g. the Vercel users who don’t want to manage another cloud provider while WebSockets are not yet supported), or who just want to support the ongoing development of pluv.io. If that’s you, sign-up here and follow the Next.js quickstart (even if you aren’t using Next.js) to get started.
But the recommended way to use pluv.io is to just self-host it yourself! 😉
Self-hosting was designed to be easy, and enables more when you have full control of your own server. Just follow the instructions for Cloudflare Workers or Node.js depending on what you’re using.
Let’s build some incredible multiplayer experiences together! 🚀
Follow me on Twitter@i3dly_dev or follow me on Bluesky@i3dly.dev for more updates!