Real-time collaboration framework for React. CRDT-powered storage, presence, cursors, live state, and undo/redo — all over WebSockets.
| Package | Description |
|---|---|
@waits/lively-types |
Shared TypeScript types and wire protocol definitions |
@waits/lively-storage |
CRDT primitives — LiveObject, LiveMap, LiveList |
@waits/lively-client |
Framework-agnostic client SDK (browser + Node/Bun) |
@waits/lively-server |
WebSocket collaboration server |
@waits/lively-react |
React hooks and providers (40+ hooks) |
@waits/lively-ui |
Pre-built components — cursors, avatars, connection badge |
@waits/lively-cli |
lively dev server CLI |
types (base)
|
storage (CRDT)
|
client (SDK) server (WebSocket)
| |
react (hooks) [standalone]
|
ui (components)
Each layer depends only on layers below it.
Server:
import { LivelyServer } from "@waits/lively-server";
const server = new LivelyServer({ port: 1999 });
await server.start();Client (React):
import { LivelyClient } from "@waits/lively-client";
import { LivelyProvider, RoomProvider } from "@waits/lively-react";
import { CursorOverlay, AvatarStack, useCursorTracking } from "@waits/lively-ui";
const client = new LivelyClient({ serverUrl: "ws://localhost:1999" });
function App() {
return (
<LivelyProvider client={client}>
<RoomProvider
roomId="my-room"
userId={user.id}
displayName={user.name}
initialStorage={{ count: 0 }}
>
<Toolbar />
<Canvas />
</RoomProvider>
</LivelyProvider>
);
}
function Canvas() {
const { ref, onMouseMove } = useCursorTracking<HTMLDivElement>();
const count = useStorage(root => root.get("count"));
const increment = useMutation(({ storage }) => {
storage.root.set("count", (storage.root.get("count") as number) + 1);
}, []);
return (
<div ref={ref} onMouseMove={onMouseMove} className="relative">
<CursorOverlay />
<p>Count: {count}</p>
<button onClick={increment}>+1</button>
</div>
);
}
function Toolbar() {
return <AvatarStack max={5} showStatus />;
}- CRDT Storage —
LiveObject,LiveMap,LiveListwith automatic conflict resolution - Presence — online/away/offline status, location tracking, custom metadata
- Cursors — real-time cursor tracking with viewport-aware follow mode
- Live State — ephemeral shared key-value state (not persisted)
- Undo/Redo — automatic inverse op capture, batch support
- Broadcast Events — custom ephemeral events between clients
- Activity Tracking — automatic inactivity detection (away/offline)
- Follow Mode — Figma-style "follow user" with viewport sync
- Suspense —
useStorageSuspenseand SSR-safeClientSideSuspense
Lively is designed to be self-hosted — you run the server on your own hardware (a home server, a VPS, or an Umbrel device) and your collaboration data never leaves it. No SaaS account, no subscription, no third party. It's an open-source, self-hosted alternative to tools like Liveblocks or a private Figma-lite for you and the people you trust.
Lively is a server-authoritative hub, not peer-to-peer. The server holds the live room (the CRDT document + presence) and relays cursors, edits, and ops to every connected client, then persists to disk. The important consequence:
Collaborators don't need to be on the same network as each other — they each just need a path to the server. The server is the hub.
The connection is always browser ⇄ server (HTTP + WebSocket). Whatever carries
that first hop is transparent to Lively, which is what makes the access model
flexible.
| Path | What it gives you | Exposure |
|---|---|---|
| Same LAN | Everyone on the home/office network reaches the server directly | Nothing leaves your network |
| Tailscale | Collaborators join over a private WireGuard mesh from anywhere | No public ports opened — the recommended remote path |
| Tor | Umbrel exposes each app as a hidden service out of the box | Works anywhere via Tor Browser; too slow for live cursors |
| Cloudflare Tunnel / reverse proxy | Public URL | DIY; only with your own auth in front |
On Umbrel, Lively rides the platform's built-in auth: every request is gated by your Umbrel login, and browsers authorize the same-origin WebSocket via that session cookie. This is secure and zero-config, but it has a deliberate v1 boundary worth stating plainly:
v1 collaboration is among people who share access to the host. On a single-account platform like Umbrel, that means everyone collaborating logs in with the same account. Great for a household or a small trusted team; not yet a way to invite an outside guest without handing over the whole server.
Participants pick a display name when they join, but there is no per-user identity separation underneath in v1.
The goal is to let others join a specific room securely, without giving up the host password — the Liveblocks/Figma "share this board" experience, on your own hardware. The building blocks are already in place:
- v1.1 — scoped room tokens. A
TokenAuthHandler(against the existingAuthHandlerinterface) issues short-lived, per-room tokens. Combined with whitelisting only the WebSocket room paths at the proxy (PROXY_AUTH_WHITELIST: "/rooms/*"on Umbrel), an invited client can join a single board over its own token while the rest of the host stays fully locked behind platform auth. This path is verified end-to-end against Umbrel's realapp_proxy. - Beyond — real identities & sharing. Per-user accounts, named participants with durable identity, per-board roles (viewer/editor), and shareable invite links with expiry — turning "people who share my server" into "people I invited to this board."
The end state: a private, self-hosted realtime layer where your data lives on your hardware, trusted people collaborate on your LAN or over Tailscale with zero exposure, and you can hand a guest a scoped link to one board without ever sharing your credentials.
| Example | Description |
|---|---|
nextjs-todo |
Collaborative todo list with LiveList, drag-and-drop, presence |
nextjs-whiteboard |
Full collaborative canvas with shapes, connectors, follow mode |
- Getting Started
- Server Guide
- Architecture
- Hooks Reference (14 docs covering all hook families)
- Components
bun install
bun run build:packages
bun run test:packages