| layout | default |
|---|---|
| title | Liveblocks - Chapter 1: Getting Started |
| nav_order | 1 |
| has_children | false |
| parent | Liveblocks - Real-Time Collaboration Deep Dive |
Welcome to Chapter 1: Getting Started. In this part of Liveblocks - Real-Time Collaboration Deep Dive, you will build an intuitive mental model first, then move into concrete implementation details and practical production tradeoffs.
Real-time collaboration has become a table-stakes feature for modern applications. Users expect to see each other's cursors, edits, and presence in real time -- the experience pioneered by Google Docs and refined by Figma and Notion. Building this from scratch requires deep expertise in WebSockets, conflict resolution algorithms, and distributed systems. Liveblocks abstracts all of that complexity into a developer-friendly SDK.
This chapter walks you through installing Liveblocks, creating your first "room" (the core collaboration primitive), and connecting multiple users to a shared real-time session.
Before writing code, it helps to understand the mental model. Liveblocks is organized around rooms. A room is a shared context where multiple users can collaborate. Every room has:
- Presence -- ephemeral state about each connected user (cursor position, selected element, etc.)
- Storage -- persistent, conflict-free shared data (documents, lists, maps)
- Events -- one-shot messages broadcast to all users in the room
flowchart TD
subgraph Room["Room: document-abc"]
P[Presence Layer]
S[Storage Layer]
E[Event Bus]
end
U1[User A] -->|cursor, selection| P
U2[User B] -->|cursor, selection| P
U1 -->|mutations| S
U2 -->|mutations| S
U1 -->|broadcast| E
E -->|receive| U2
S --> DB[(Persistent Storage)]
P --> MEM[(In-Memory)]
style Room fill:#e3f2fd,stroke:#1565c0
style P fill:#fff3e0,stroke:#e65100
style S fill:#e8f5e9,stroke:#2e7d32
style E fill:#fce4ec,stroke:#c62828
Sign up at liveblocks.io and create a new project. You will receive two keys:
| Key Type | Format | Usage |
|---|---|---|
| Public Key | pk_live_xxx or pk_dev_xxx |
Client-side, safe to expose |
| Secret Key | sk_live_xxx or sk_dev_xxx |
Server-side only, never expose |
The _dev_ variants connect to a development environment with relaxed rate limits and no billing.
Liveblocks is modular. Install only what you need:
# Core client (framework-agnostic)
npm install @liveblocks/client
# React bindings (if using React)
npm install @liveblocks/react
# React UI components (pre-built collaboration UI)
npm install @liveblocks/react-ui
# Node.js server utilities (for authentication)
npm install @liveblocks/nodeFor a typical React or Next.js project, install everything at once:
npm install @liveblocks/client @liveblocks/react @liveblocks/react-ui @liveblocks/nodeLiveblocks provides a CLI for project scaffolding and schema management:
npx create-liveblocks-app@latest --initThis generates a liveblocks.config.ts file with type-safe configuration.
The Liveblocks client is the entry point to all functionality. It manages WebSocket connections and provides access to rooms.
// liveblocks.config.ts
import { createClient } from "@liveblocks/client";
import { createRoomContext } from "@liveblocks/react";
// Option 1: Public key authentication (development)
const client = createClient({
publicApiKey: "pk_dev_your_key_here",
});
// Option 2: Token-based authentication (production)
const client = createClient({
authEndpoint: "/api/liveblocks-auth",
});Liveblocks is fully type-safe. Define your presence and storage types upfront:
// liveblocks.config.ts
import { createClient } from "@liveblocks/client";
import { createRoomContext } from "@liveblocks/react";
import { LiveList, LiveObject, LiveMap } from "@liveblocks/client";
// What each user broadcasts about themselves
type Presence = {
cursor: { x: number; y: number } | null;
selectedId: string | null;
name: string;
color: string;
};
// Shared persistent state
type Storage = {
shapes: LiveMap<string, LiveObject<{
x: number;
y: number;
width: number;
height: number;
fill: string;
}>>;
layers: LiveList<string>;
};
// Custom broadcast events
type RoomEvent =
| { type: "EMOJI_REACTION"; emoji: string; x: number; y: number }
| { type: "POINTER_MOVE"; x: number; y: number };
// User metadata (from your auth system)
type UserMeta = {
id: string;
info: {
name: string;
avatar: string;
color: string;
};
};
// Thread metadata for comments
type ThreadMetadata = {
resolved: boolean;
x: number;
y: number;
};
const client = createClient({
publicApiKey: "pk_dev_your_key_here",
});
export const {
suspense: {
RoomProvider,
useRoom,
useMyPresence,
useSelf,
useOthers,
useOthersMapped,
useOthersConnectionIds,
useOther,
useBroadcastEvent,
useEventListener,
useStorage,
useMutation,
useHistory,
useUndo,
useRedo,
useCanUndo,
useCanRedo,
useBatch,
useStatus,
useThreads,
useCreateThread,
useCreateComment,
},
} = createRoomContext<Presence, Storage, UserMeta, RoomEvent, ThreadMetadata>(client);Starting with Liveblocks 1.x, you can also use a global configuration approach with liveblocks.config.ts:
// liveblocks.config.ts (global approach)
declare global {
interface Liveblocks {
Presence: {
cursor: { x: number; y: number } | null;
name: string;
};
Storage: {
todos: LiveList<LiveObject<{
text: string;
completed: boolean;
author: string;
}>>;
};
UserMeta: {
id: string;
info: {
name: string;
avatar: string;
};
};
RoomEvent: {
type: "NOTIFICATION";
message: string;
};
ThreadMetadata: {
resolved: boolean;
x: number;
y: number;
};
}
}
export {};Every collaborative feature lives inside a RoomProvider. This component establishes a WebSocket connection to a specific room:
// app/page.tsx (Next.js App Router)
"use client";
import { RoomProvider } from "../liveblocks.config";
import { LiveList } from "@liveblocks/client";
import { ClientSideSuspense } from "@liveblocks/react";
import { CollaborativeEditor } from "./CollaborativeEditor";
export default function Page() {
return (
<RoomProvider
id="my-first-room"
initialPresence={{
cursor: null,
selectedId: null,
name: "Anonymous",
color: "#FF0000",
}}
initialStorage={{
shapes: new LiveMap(),
layers: new LiveList([]),
}}
>
<ClientSideSuspense fallback={<div>Connecting...</div>}>
{() => <CollaborativeEditor />}
</ClientSideSuspense>
</RoomProvider>
);
}Room IDs are strings that identify a collaboration session. Common patterns include:
| Pattern | Example | Use Case |
|---|---|---|
| Static | "main-canvas" |
Single shared workspace |
| Document-based | "doc:${documentId}" |
Per-document collaboration |
| User-scoped | "user:${userId}:drafts" |
Personal spaces |
| Hierarchical | "org:${orgId}:project:${projectId}" |
Multi-tenant apps |
// Dynamic room IDs based on URL
function DocumentPage({ params }: { params: { docId: string } }) {
return (
<RoomProvider
id={`document-${params.docId}`}
initialPresence={{ cursor: null, name: "User" }}
>
<Editor />
</RoomProvider>
);
}Let us build a minimal component that shows connected users and tracks cursor positions:
// CollaborativeEditor.tsx
"use client";
import { useMyPresence, useOthers, useSelf } from "../liveblocks.config";
import { useCallback } from "react";
export function CollaborativeEditor() {
const [myPresence, updateMyPresence] = useMyPresence();
const others = useOthers();
const self = useSelf();
const handlePointerMove = useCallback(
(event: React.PointerEvent) => {
const rect = event.currentTarget.getBoundingClientRect();
updateMyPresence({
cursor: {
x: event.clientX - rect.left,
y: event.clientY - rect.top,
},
});
},
[updateMyPresence]
);
const handlePointerLeave = useCallback(() => {
updateMyPresence({ cursor: null });
}, [updateMyPresence]);
return (
<div
onPointerMove={handlePointerMove}
onPointerLeave={handlePointerLeave}
style={{
position: "relative",
width: "100%",
height: "100vh",
background: "#fafafa",
}}
>
{/* Show connection status */}
<div style={{ padding: 16 }}>
<p>Connected as: {self?.info?.name ?? "Anonymous"}</p>
<p>Users in room: {others.length + 1}</p>
</div>
{/* Render other users' cursors */}
{others.map(({ connectionId, presence, info }) => {
if (!presence.cursor) return null;
return (
<Cursor
key={connectionId}
x={presence.cursor.x}
y={presence.cursor.y}
name={info?.name ?? `User ${connectionId}`}
color={info?.color ?? "#000"}
/>
);
})}
</div>
);
}
function Cursor({ x, y, name, color }: {
x: number;
y: number;
name: string;
color: string;
}) {
return (
<div
style={{
position: "absolute",
left: x,
top: y,
pointerEvents: "none",
transform: "translate(-4px, -4px)",
}}
>
<svg width="24" height="36" viewBox="0 0 24 36" fill="none">
<path
d="M5.65 0L0 31.56L8.85 22.95L18.82 36L23.15 33.27L13.15 20.27L24.65 18.78L5.65 0Z"
fill={color}
/>
</svg>
<span
style={{
position: "absolute",
top: 24,
left: 8,
background: color,
color: "white",
padding: "2px 6px",
borderRadius: 4,
fontSize: 12,
whiteSpace: "nowrap",
}}
>
{name}
</span>
</div>
);
}Understanding how connections work is important for building robust apps:
stateDiagram-v2
[*] --> Initial
Initial --> Connecting: RoomProvider mounts
Connecting --> Connected: WebSocket opened
Connected --> Reconnecting: Connection lost
Reconnecting --> Connected: Reconnected
Reconnecting --> Disconnected: Max retries exceeded
Connected --> Disconnected: RoomProvider unmounts
Disconnected --> [*]
note right of Connecting
SDK authenticates and
syncs initial state
end note
note right of Reconnecting
Automatic retry with
exponential backoff
end note
You can monitor connection status in your components:
import { useStatus } from "../liveblocks.config";
function ConnectionIndicator() {
const status = useStatus();
const statusConfig = {
initial: { color: "gray", label: "Initializing" },
connecting: { color: "yellow", label: "Connecting..." },
connected: { color: "green", label: "Connected" },
reconnecting: { color: "orange", label: "Reconnecting..." },
disconnected: { color: "red", label: "Disconnected" },
};
const { color, label } = statusConfig[status];
return (
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<div
style={{
width: 8,
height: 8,
borderRadius: "50%",
backgroundColor: color,
}}
/>
<span>{label}</span>
</div>
);
}| Aspect | Development | Production |
|---|---|---|
| API Key | pk_dev_xxx |
pk_live_xxx |
| Auth | Public key (no server) | Token-based (server required) |
| Rate Limits | Relaxed | Standard plan limits |
| Data | Ephemeral (cleared periodically) | Persistent |
| Dashboard | Dev project | Live project |
# .env.local
NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY=pk_dev_xxx
LIVEBLOCKS_SECRET_KEY=sk_dev_xxx// liveblocks.config.ts
const client = createClient({
publicApiKey: process.env.NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY!,
});For production, you need a server endpoint that issues access tokens:
// app/api/liveblocks-auth/route.ts (Next.js App Router)
import { Liveblocks } from "@liveblocks/node";
import { NextRequest } from "next/server";
const liveblocks = new Liveblocks({
secret: process.env.LIVEBLOCKS_SECRET_KEY!,
});
export async function POST(request: NextRequest) {
// Get user from your session/auth system
const user = await getUserFromSession(request);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
const session = liveblocks.prepareSession(user.id, {
userInfo: {
name: user.name,
avatar: user.avatarUrl,
color: user.color,
},
});
// Grant access to specific rooms
session.allow(`document-*`, session.FULL_ACCESS);
session.allow(`org:${user.orgId}:*`, session.FULL_ACCESS);
const { status, body } = await session.authorize();
return new Response(body, { status });
}Then update your client to use the auth endpoint:
const client = createClient({
authEndpoint: "/api/liveblocks-auth",
});A well-organized Liveblocks project typically looks like this:
src/
liveblocks.config.ts # Client + type definitions
app/
api/
liveblocks-auth/
route.ts # Auth endpoint
rooms/
[roomId]/
page.tsx # Room page with RoomProvider
components/
collaboration/
Cursors.tsx # Cursor rendering
AvatarStack.tsx # Who's online
ConnectionStatus.tsx # Connection indicator
editor/
Canvas.tsx # Main editor
Toolbar.tsx # Editor toolbar
Open two browser tabs pointing to the same room. You should see:
- Both tabs connect to the same room
- Moving the cursor in one tab shows it in the other
- The user count updates in real time
- Closing one tab removes that user's cursor
// Quick smoke test
import { createClient } from "@liveblocks/client";
async function testConnection() {
const client = createClient({
publicApiKey: "pk_dev_your_key_here",
});
const { room, leave } = client.enterRoom("test-room", {
initialPresence: { cursor: null },
});
room.subscribe("status", (status) => {
console.log("Connection status:", status);
if (status === "connected") {
console.log("Successfully connected to Liveblocks!");
leave();
}
});
}
testConnection();In this chapter you learned:
- Liveblocks architecture: rooms, presence, storage, and events form the core primitives
- Installation: modular packages let you install only what you need
- Type safety: defining Presence, Storage, UserMeta, and RoomEvent types gives you full IntelliSense
- RoomProvider: the gateway component that establishes WebSocket connections
- Connection lifecycle: Liveblocks handles reconnection automatically with exponential backoff
- Authentication: public keys for development, token-based auth for production
- Everything in Liveblocks revolves around rooms -- isolated collaboration contexts identified by string IDs.
- The SDK is fully type-safe -- define your types once and get autocomplete everywhere.
- Start with public key auth for development, switch to token-based auth before going to production.
- The RoomProvider component handles connection management, including automatic reconnection.
- Liveblocks is framework-agnostic at the core, with first-class React bindings on top.
In the next chapter, we will dive deep into Presence & Awareness -- learning how to build rich "who's online" experiences, live cursors, and real-time broadcasting.
Built with insights from the Liveblocks platform.
Most teams struggle here because the hard part is not writing more code, but deciding clear boundaries for liveblocks, color, client so behavior stays predictable as complexity grows.
In practical terms, this chapter helps you avoid three common failures:
- coupling core logic too tightly to one implementation path
- missing the handoff boundaries between setup, execution, and validation
- shipping changes without clear rollback or observability strategy
After working through this chapter, you should be able to reason about Chapter 1: Getting Started as an operating subsystem inside Liveblocks - Real-Time Collaboration Deep Dive, with explicit contracts for inputs, state transitions, and outputs.
Use the implementation notes around name, cursor, status as your checklist when adapting these patterns to your own repository.
Under the hood, Chapter 1: Getting Started usually follows a repeatable control path:
- Context bootstrap: initialize runtime config and prerequisites for
liveblocks. - Input normalization: shape incoming data so
colorreceives stable contracts. - Core execution: run the main logic branch and propagate intermediate state through
client. - Policy and safety checks: enforce limits, auth scopes, and failure boundaries.
- Output composition: return canonical result payloads for downstream consumers.
- Operational telemetry: emit logs/metrics needed for debugging and performance tuning.
When debugging, walk this sequence in order and confirm each stage has explicit success/failure conditions.
Use the following upstream sources to verify implementation details while reading this chapter:
- Liveblocks GitHub Repository
Why it matters: authoritative reference on
Liveblocks GitHub Repository(github.com). - Liveblocks Product Site
Why it matters: authoritative reference on
Liveblocks Product Site(liveblocks.io). - Liveblocks Documentation
Why it matters: authoritative reference on
Liveblocks Documentation(liveblocks.io).
Suggested trace strategy:
- search upstream code for
liveblocksandcolorto map concrete implementation paths - compare docs claims against actual runtime/config code before reusing patterns in production