@onyx/realtime
Yjs collaboration and presence on Durable Objects.
@onyx/realtime provides realtime collaboration and presence. One room per
organizationId:page speaks the Yjs sync and awareness protocols over a
WebSocket: the shared Y.Doc carries page state (task lists, the notes
document) and awareness carries who is on the page. Document snapshots
persist across restarts, connections are authenticated and scoped to the
caller's active organization by the existing Worker wiring, and local dev
simulates the Durable Object. You only work with the React client.
Providing a room
Mount PageRoomProvider once per page so presence and page features share a
single WebSocket. The app layout in apps/web keys it on the pathname:
// apps/web/src/routes/_app.tsx
import { PageRoomProvider } from '@onyx/realtime/client';
<PageRoomProvider
page={pathname}
user={{ id: user.id, image: user.image ?? null, name: user.name }}
>
{/* page content, presence avatars, ... */}
</PageRoomProvider>;The provider is preconfigured with the right connection URL, so consumers never build URLs themselves.
Consuming a room
usePageRoomContext() returns { room, users }. room is null until the
connection is established on the client, and it exposes doc (the shared
Y.Doc), provider, and page. Always check room.page before binding
anything to the doc: during navigation the shared provider swaps rooms while
the outgoing page is still mounted.
// apps/web/src/routes/_app/notes.tsx
import { usePageRoomContext } from '@onyx/realtime/client';
function NotesPage() {
const { room } = usePageRoomContext();
// Treat a room for a different page as "not connected yet".
const notesRoom = room !== null && room.page === Route.fullPath ? room : null;
// Bind notesRoom.doc to Tiptap Collaboration, a shared task list, etc.
}Presence is just the users array:
const { users } = usePageRoomContext();
// Render an avatar per PresenceUser currently on this page.PresenceUser (id, name, image) and the isPresenceUser guard for
validating awareness states from other clients come from
@onyx/realtime/types.
Outside the provider
usePageRoom (also in @onyx/realtime/client) is the underlying hook if you
need a room connection outside the shared provider, for example a second room
on the same page. Prefer the provider for normal pages so everything shares
one WebSocket.
No secrets are required. Authentication reuses the app session.