Introduction
pluv.io 3.0 is finally out, and with it brings major developer experience, stability and usability enhancements. Here’s what’s new:
- 🚀 Yjs Provider: Plug into collaborative editors using our official Yjs connection provider.
- ⚠️ Streamlined Subscriptions (Breaking): A more intuitive way to subscribe to
PluvRoom
data.
- ⚠️ Improved CRDT Initialization (Breaking): Simpler, native-like usage of Yjs and Loro.
- 🧠 Inferred CRDT Document Types: Stronger typing for your
useDoc
andgetDoc
usage.
- 🔧 Major Stability Fixes: Connection, storage, and sync bugs resolved.
- 📘 Revamped Documentation: Easier to navigate, easier to understand.
Yjs Provider
Yjs defines a connection provider interface used by many collaborative libraries. While pluv.io has long been Yjs-compatible, it was still difficult to build more complex collaborative features, as pluv.io had not provided a compatible provider.
As of @pluv/crdt-yjs@2.2.0, pluv.io ships with its own official Yjs provider, allowing easy integration with popular rich text editors that support Yjs. The Yjs docs currently list six examples of editor bindings, and pluv.io adds docs for BlockNote, Lexical and Slate - bringing a total of nine supported editors.
Here’s a sample using BlockNote with pluv.io’s Yjs provider:
import { BlockNoteView } from "@blocknote/mantine"; import { useCreateBlockNote } from "@blocknote/react"; import { yjs } from "@pluv/crdt-yjs"; import { useMemo } from "react"; import { useRoom, useStorage } from "./pluv"; // ... const room = useRoom(); const [, fragment] = useStorage(); const provider = useMemo(() => { return yjs.provider({ room, // The presence field we choose to store // Yjs Awaneness data onto presenceField: "myAwareness", }); }, [room]); // ... Later on const editor = useCreateBlockNote({ collaboration: { provider, fragment, user: { name: "John Doe", color: "#00FF00" }, }, }); return <BlockNoteView editor={editor} />;
Streamlined Subscriptions (Breaking)
We’ve unified how you subscribe to room data with a new
subscribe
API. Previously, PluvRoom
methods like .event
, .storage
, and .other
were inconsistent. Now they’re organized under a single room.subscribe
namespace for improved discoverability and DX.Migration is simple:
const room = client.createRoom("example-room"); // Before room.subscribe("connection", (state) => {}); room.subscribe("my-presence", (myPresence) => {}); room.subscribe("myself", (myself) => {}); room.subscribe("others", (others) => {}); room.subscribe("storage-loaded", (storageLoaded) => {}); room.event.receiveMessage((event) => {}); room.storage.messages((messages) => {}); room.storageRoot((storage) => {}); room.other(("id_...", other) => {}); // After room.subscribe.connection((state) => {}); room.subscribe.myPresence((myPresence) => {}); room.subscribe.myself((myself) => {}); room.subscribe.others((others) => {}); room.subscribe.storageLoaded((storageLoaded) => {}); room.subscribe.event.receiveMessage((event) => {}); room.subscribe.storage.messages((messages) => {}); room.subscribe.storageRoot((storage) => {}); room.subscribe.other(("id_...", other) => {});
See the new v3 Breaking Changes migration guide for full details.
Improved CRDT Initialization (Breaking)
Our CRDT helpers in
@pluv/crdt-yjs
and @pluv/crdt-loro
focused on improving the ergonomics of working with the underlying CRDT libraries (yjs
and loro-crdt
). While elegant, these abstractions added friction to working with these CRDTs.In 3.0, we’ve simplified this:
- Declaring top-level storage fields now require using a document builder exposed in the
.doc
function, while nested types remain unchanged.
import { yjs } from "@pluv/crdt-yjs"; import { Text as YText } from "yjs"; // Before yjs.doc(() => ({ groceries: yjs.array<string>([]), messages: yjs.array([ yjs.text("hello world"), ]), })); // After yjs.doc((t) => ({ // Top-level fields now use the `t` builder groceries: t.array<string>("groceries", []), messages: t.array("messages", [ // Nested fields continue using the yjs utility yjs.text("hello world"), ]), }));
- The
yjs
type-helpers return their native Yjs shared-types, and theloro
type-helpers return their native Loro container-types.
import { yjs } from "@pluv/crdt-yjs"; import { Text as YText } from "yjs"; import { useStorage } from "./pluv"; const [messages, yArray] = useStorage("messages"); // This means that these are ultimately the same yArray.push([yjs.text("hello world")]); yArray.push([new YText("hello world")]);
These changes not only improve the usability of these types, but were also necessary to enable assigning top-level shared-types to an IIFE for Slate.
Inferred CRDT Document Types
useDoc
and PluvRoom.getDoc
are now properly type-inferred and return their correctly-typed CRDT documents - no more manual typing or casting necessary.import { createClient } from "@pluv/client"; import { yjs } from "@pluv/crdt-yjs"; import { Doc as YDoc } from "yjs"; const io = createClient({ // ... initialStorage: yjs.doc((t) => ({ /* ... */ }), }); const room = io.createRoom("example-room"); const doc = room.getDoc().value; // ^? const doc: YDoc;
Major Stability Fixes
We’ve resolved a number of long-standing issues that impacted connection reliability and storage behavior:
- 🧵 Fixed a race condition during the first
initialStorage
sync, sometimes causing documents to not be able to properly sync.
- 🗃️ Fixed race condition in
@pluv/addon-indexeddb
causing storage diffs to apply incorrectly between the browser’s IndexedDB and the server’s emitted sync state.
- 💾 Fixed persistence issues in Cloudflare Durable Object-backed SQLite storage.
- 🧱 Fixed shared object reference errors in
@pluv/platform-cloudflare
between multiple Durable Objects.
Revamped Documentation
We’ve rebuilt the pluv.io documentation to reflect the stability and maturity of the platform. Previously, the docs were written as we shipped features - often without the time to focus on making the docs easy to understand.
Now, with a much more stable API surface, we’ve focused on:
- Better navigation
- Clearer examples
- More consistent terminology
There’s still room to grow, but this marks a big step forward in our commitment to great DX.
Want to help? Docs live on GitHub as
.mdx
files. Feel free to open a PR: Free Tier
Starting today, pluv.io offered a fully managed free tier so taht you can start building multiplayer experiences immediately.
Just signup to start using
@pluv/platform-pluv
, and follow our quickstart guide to go live within minutes.Want full control? Self-hosting pluv.io remains completely free with zero restrictions. Whether you deploy on Cloudflare Workers or Node.js, we’ll continue to support and recommend running it yourself if that’s your preference.
Let’s build some incredible multiplayer experiences together! 🚀
Follow me on Twitter@i3dly_dev or Bluesky@i3dly.dev for more updates!