Sign in

Storage

Storage is the persistent document inside Sync. Multiple humans and agents can edit it at the same time, and conflict resolution merges simultaneous changes without losing unrelated edits. Documents remain after every user disconnects, unlike Presence, which is temporary.

Conflict-free data types

Storage documents are built from conflict-free data types, each resolving simultaneous edits differently.

Data typeDescriptionExample value
LiveObjectA record of named values, similar to a JavaScript object.LiveObject({ x: 50, y: 100, color: "red" })
LiveListAn ordered collection, similar to a JavaScript array.LiveList(["rectangle-1", "circle-2"])
LiveMapA key-value collection, similar to a JavaScript map.LiveMap([["pierre", 152], ["alicia", 186]])
LiveTextA collection of text nodes, storing formatting data.LiveText(["Hello world", { format: "bold" }])
LiveFileAn immutable reference to an uploaded file.LiveFile({ name: "photo.png", size: 984, ... })

LiveObject

LiveObject is similar to a JavaScript object that is synchronized on all clients—users can update different properties at the same time, and changes are merged together. Use it for storing records with fixed key names and where the values don’t necessarily have the same types, for example, a shape on a canvas.

import { LiveObject } from "@liveblocks/client";
// Defining a LiveObjectconst shape = new LiveObject({ x: 50, y: 100, color: "red",});
// Example methodsshape.get("color");shape.set("color", "blue");shape.update({ x: 100, y: 200 });

LiveList

LiveList is similar to a JavaScript array that is synchronized across clients—users can delete, insert, and move items at the same time as others, and changes are merged together. Use it for storing an ordered collection of items, for example a list of layers on a canvas.

import { LiveList } from "@liveblocks/client";
// Defining a LiveListconst layers = new LiveList(["rectangle-1", "circle-2"]);
// Example methodslayers.push("triangle-3");layers.move(0, 2);layers.remove(1);

LiveMap

LiveMap is similar to a JavaScript map that is synchronized across clients—users can update, delete, and insert items at the same time as others, and changes are merged together automatically. Use it for storing key-value pairs, for example a list of users and their upvotes.

import { LiveMap } from "@liveblocks/client";
// Defining a LiveMapconst users = new LiveMap([ ["pierre", 152], ["alicia", 185],]);
// Example methodsusers.keys();users.delete("alicia");users.set("pierre", 153);

LiveText

LiveText is used to store collaborative rich text data—users can insert, replace, and format text at the same time as others, and changes are merged together automatically. It’s generally most useful when used internally by a text editing integration, but it can be handled directly too.

import { LiveText } from "@liveblocks/client";
// Defining a LiveTextconst text = new LiveText(["Hello", { format: "bold" }]);
// Example methodstext.insert(5, " world");text.replace(0, 5, "Hi");text.format(0, 2, { format: "italic" });

LiveText supports Tiptap, BlockNote, ProseMirror, and CodeMirror.

LiveFile

LiveFile is used to store a reference to a file uploaded to Liveblocks. Any kind of file can be uploaded with useUploadFile and attached to your data tree.

import { LiveFile } from "@liveblocks/client";import { useUploadFile } from "@liveblocks/react/suspense";
const uploadFile = useUploadFile();const liveFile = await uploadFile(file);
// LiveFile<{ id: "fl_xxx", name: "photo.png", size: 12345, mimeType: "image/png" }>console.log(liveFile);

Nesting data types

LiveObject, LiveList, and LiveMap can each contain other live structures, allowing you to build a full JSON-like document tree. Each part of the structure can be edited independently, and changes are merged together automatically. For example, a LiveList of shapes may contain LiveObject shapes.

import { LiveMap, LiveObject } from "@liveblocks/client";
const shape = new LiveObject({ x: 50, y: 100, color: "red",});
const shapes = new LiveList([shape]);

In the following complex example, all data types are used, and some modifications are made.

Using Storage

Setting up

Before using Storage, first decide on the shape of your document, and set the Storage type in your Liveblocks config file. This makes every hook and mutation in your app fully typed, with TypeScript. In this example, this canvas document holds a list of shapes.

liveblocks.config.ts
import { LiveList, LiveObject } from "@liveblocks/client";
type Shape = LiveObject<{ x: number; y: number; color: string;}>;
declare global { interface Liveblocks { Storage: { shapes: LiveList<Shape>; }; }}

Next, in React, create an initial document with initialStorage on RoomProvider. It’s applied once, the first time a room is entered—after that, the stored document is the source of truth.

import { LiveList, LiveObject } from "@liveblocks/client";import { RoomProvider } from "@liveblocks/react/suspense";
function App({ children }: { children: React.ReactNode }) { return ( <RoomProvider id="my-room" initialStorage={{ shapes: new LiveList([new LiveObject({ x: 50, y: 100, color: "red" })]), }} > {children} </RoomProvider> );}

Now that the type has been set, and the initial document has been created, you can now start to use Storage in your application.

Reading data

useStorage reads a part of the document, returning it as an immutable JSON value. For example, a LiveList is converted into a plain JavaScript array, making it easy to render it in your component. Using a selector function like (root) => root.shapes, you can fetch only the part of the document you need, and it will re-render in realtime as other users make changes.

import { useStorage } from "@liveblocks/react/suspense";
function Canvas() { const shapes = useStorage((root) => root.shapes);
return ( <> {shapes.map((shape, index) => ( <Shape key={index} x={shape.x} y={shape.y} color={shape.color} /> ))} </> );}

Updating data

useMutation creates a callback that modifies the document. Changes apply instantly for the current user, sync to everyone else in realtime, and merge with any simultaneous edits.

import { useMutation } from "@liveblocks/react/suspense";import { LiveObject } from "@liveblocks/client";
function Toolbar() { const addShape = useMutation(({ storage }) => { const shapes = storage.get("shapes");
const newShape = new LiveObject({ x: 150, y: 250, color: "orange", });
shapes.push(newShape); }, []);
return <button onClick={() => addShape()}>Add shape</button>;}

Mutations can accept extra arguments after the context object, so you can pass in values when calling them. The following mutation takes a color property, and sets it on a shape at a given index.

import { useMutation } from "@liveblocks/react/suspense";
function ColorPicker({ index }: { index: number }) { const setColor = useMutation( ({ storage }, color: string) => { const shape = storage.get("shapes").get(index);
if (shape) { shape.set("color", color); } }, [index] );
return <input type="color" onChange={(e) => setColor(e.target.value)} />;}

Storage can also be modified from your back end and by AI agents, learn more under server-side editing and agentic editing.

Creating a multiplayer component

Putting together useStorage and useMutation, you can create editable multiplayer components in your app. For example, a select menu that updates in realtime for all users.

liveblocks.config.ts
declare global {  interface Liveblocks {    Storage: {      priority: "low" | "medium" | "high";    };  }}
import { useStorage } from "@liveblocks/react/suspense";
function SelectPriority() { const priority = useStorage((root) => root.priority);
const setPriority = useMutation(({ storage }, priority: string) => { storage.set("priority", priority); });
return ( <select value={property} onChange={(e) => setProperty(e.target.value)}> <option value="low">Low</option> <option value="medium">Medium</option> <option value="high">High</option> </select> );}

Undo and redo changes

When using Storage, each user has an independent undo stack which is saved in memory until the user leaves the page. Using undo reverts the user’s Storage changes without reversing work made by other collaborators.

The useUndo and useRedo hooks can be used to trigger undo/redo actions. Additionally useCanUndo and useCanRedo can be used to check if there are any actions to undo/redo, helpful for disabling buttons. Put these together to create undo and redo buttons.

import {  useCanRedo,  useCanUndo,  useRedo,  useUndo,} from "@liveblocks/react/suspense";
function HistoryButtons() { const undo = useUndo(); const redo = useRedo(); const canUndo = useCanUndo(); const canRedo = useCanRedo();
return ( <> <button onClick={undo} disabled={!canUndo}> ↩️ Undo </button> <button onClick={redo} disabled={!canRedo}> ↪️ Redo </button> </> );}

Pause and resume history

You can choose to pause undo/redo history when a single gesture should count as a single undo step. For example, picture dragging a shape across a canvas—it moves 200 pixels, but you wouldn’t want to press undo 200 times. Pausing history allows you to merge this drag action into one undo step. Intermediate positions still sync to other clients, but undo restores the position from before the drag.

import { useHistory, useMutation } from "@liveblocks/react/suspense";
function Shape({ index }: { index: number }) { const { pause, resume } = useHistory();
const moveShape = useMutation(({ storage }, x: number, y: number) => { const shape = storage.get("shapes").get(index);
if (shape) { shape.update({ x, y }); } });
return ( <ShapeComponent onDragStart={() => pause()} onDragEnd={() => resume()} onDragMove={moveShape} /> );}

useHistory also exposes disable for when a local change should not enter the stack at all.

Presence history

Undo only affects Storage by default, but it can sometimes be helpful to revert Presence changes too. For example, when a shape is dragged on a canvas, a user’s Presence may be visible around the shape. If the shape is moved back with undo, it makes sense to move the user’s selection back too. Pass { addToHistory: true } when updating Presence to add a value to the undo/redo stack.

import { useHistory, useMutation } from "@liveblocks/react/suspense";
function Shape({ index }: { index: number }) { const { pause, resume } = useHistory();
const moveShape = useMutation( ({ storage, setMyPresence }, x: number, y: number) => { const shape = storage.get("shapes").get(index);
if (shape) { shape.update({ x, y }); setMyPresence({ selectedShapeIndex: index }, { addToHistory: true }); } } );
return ( <ShapeComponent onDragStart={() => pause()} onDragEnd={() => resume()} onDragMove={moveShape} /> );}

Note that useMutation provides a setMyPresence helper, so that you don't need to import useUpdateMyPresence separately.

Version history

Storage supports version history, allowing you to snapshot document states and revert to previous versions of the document.

import { Liveblocks } from "@liveblocks/node";
const liveblocks = new Liveblocks({ secret: "",});
const { data } = await liveblocks.createVersionHistorySnapshot("my-room");

To learn more about how to do this, read the version history page.

Server-side editing

You can read and edit Storage directly from the server, using conflict-free data types or JSON Patch.

import { Liveblocks } from "@liveblocks/node";
const liveblocks = new Liveblocks({ secret: "",});
await liveblocks.mutateStorage("my-room", ({ root }) => { const shapes = root.get("shapes");
const newShape = new LiveObject({ x: 250, y: 300, color: "purple", });
shapes.push(newShape);});

To learn more about how to do this, read the server-side editing page.

Agentic editing

AI agents can generate changes for your document, using Node.js methods or JSON Patch.

import { LiveObject } from "@liveblocks/client";import { Liveblocks } from "@liveblocks/node";import { generateText, Output } from "ai";import { z } from "zod";
const liveblocks = new Liveblocks({ secret: "",});
await liveblocks.mutateStorage("my-room-id", async ({ root }) => { const shapes = root.get("shapes");
const { output: shape } = await generateText({ model: "openai/gpt-5.6-sol", output: Output.object({ schema: z.object({ x: z.number(), y: z.number(), color: z.string(), }), }), prompt: `Create a rectangle for the canvas. Here are current shapes: ${shapes.toJSON()}`, });
shapes.set(new LiveObject(shape));});

To learn more about how to do this, read the agentic editing page.