diff --git a/docs/pages/ready-made-features/multiplayer/sync-engine/liveblocks-storage.mdx b/docs/pages/ready-made-features/multiplayer/sync-engine/liveblocks-storage.mdx index 96355f7536c..0d5b56f81c3 100644 --- a/docs/pages/ready-made-features/multiplayer/sync-engine/liveblocks-storage.mdx +++ b/docs/pages/ready-made-features/multiplayer/sync-engine/liveblocks-storage.mdx @@ -244,6 +244,72 @@ function StorageStatusBadge() { A number of other hooks are available to help you with different aspects of multiplayer development, check our API reference for more information. +## Server-side modifications + +Storage’s presence and conflict-free data structures can be modified through our +[Node.js package](/docs/api-reference/liveblocks-node) or via +[REST API](/docs/api-reference/rest-api-endpoints). + +### Presence [#server-side-modifications-presence] + +Presence can be modified with +[`liveblocks.setPresence`](/docs/api-reference/liveblocks-node#set-rooms-roomId-presence), +allowing you set an ephemeral value that will expire after a certain amount of +time. + +```ts +await liveblocks.setPresence("my-room-id", { + userId: "agent-123", + data: { + status: "active", + cursor: { x: 100, y: 200 }, + }, + userInfo: { + name: "AI Assistant", + avatar: "https://example.com/avatar.png", + }, + ttl: 60, +}); +``` + +The same operation can be performed in other languages with the +[Set ephemeral presence REST API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-presence).\ + +### Conflict-free data structures [#server-side-modifications-conflict-free-data-structures] + +Conflict-free data structures can be modified with +[`liveblocks.mutateStorage`](/docs/api-reference/liveblocks-node#mutate-storage), +allowing you to modify the data structures similarly to on the client-side. + +```ts +await liveblocks.mutateStorage( + "my-room-id", + + ({ root }) => { + root.get("list").push("item3"); + } +); +``` + +The same operation can be performed using the +[Apply JSON Patch to Storage REST API](/docs/api-reference/rest-api-endpoints#patch-rooms-roomId-storage-json-patch). +We have a guide on +[Modifying Storage via REST API with JSON Patch](/docs/guides/modifying-storage-via-rest-api-with-json-patch) +that covers this in more detail. + +### Broadcast [#server-side-modifications-broadcast] + +Broadcast can be performed with +[`liveblocks.broadcastEvent`](/docs/api-reference/liveblocks-node#post-broadcast-event), +allowing you to send events to all connected clients. + +```ts +await liveblocks.broadcastEvent("my-room-id", { type: "PLAY_VIDEO" }); +``` + +The same operation can be performed using the +[Broadcast event to a room REST API](/docs/api-reference/rest-api-endpoints#post-broadcast-event). + ## API Reference ### Presence diff --git a/docs/references/v2.openapi.json b/docs/references/v2.openapi.json index a3517956558..7ccea9bb77b 100644 --- a/docs/references/v2.openapi.json +++ b/docs/references/v2.openapi.json @@ -1124,6 +1124,122 @@ "description": "This endpoint deletes all of the room’s Storage data. Calling this endpoint will disconnect all users from the room if there are any. Corresponds to [`liveblocks.deleteStorageDocument`](/docs/api-reference/liveblocks-node#delete-rooms-roomId-storage).\n" } }, + "/rooms/{roomId}/storage/json-patch": { + "patch": { + "summary": "Apply JSON Patch to Storage", + "description": "Applies a sequence of [JSON Patch](https://datatracker.ietf.org/doc/html/rfc6902) operations to the room's Storage document, useful for modifying Storage. Operations are applied in order; if any operation fails, the document is not changed and a 422 response with a helpful message is returned.\n\n**Paths and data types:** Be as specific as possible with your target path. Every parent in the chain of path segments must be a LiveObject, LiveList, or LiveMap. Complex nested objects passed in `add` or `replace` operations are automatically converted to LiveObjects and LiveLists.\n\n**Performance:** For large Storage documents, applying a patch can be expensive because the full state is reconstructed on the server to apply the operations. Very large documents may not be suitable for this endpoint.\n\nFor a **full guide with examples**, see [Modifying storage via REST API with JSON Patch](/docs/guides/modifying-storage-via-rest-api-with-json-patch).", + "tags": ["Storage"], + "operationId": "patch-rooms-roomId-storage-json-patch", + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "roomId", + "in": "path", + "required": true, + "description": "ID of the room" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "required": ["op", "path"], + "properties": { + "op": { + "type": "string", + "enum": [ + "add", + "remove", + "replace", + "move", + "copy", + "test" + ], + "description": "The operation to perform (RFC 6902)." + }, + "path": { + "type": "string", + "description": "A JSON Pointer to the target location (RFC 6901). Must start with \"/\"." + }, + "from": { + "type": "string", + "description": "Required for \"move\" and \"copy\". A JSON Pointer to the source location." + }, + "value": { + "description": "Required for \"add\", \"replace\", and \"test\". The value to add, the replacement value, or the value to test against." + } + }, + "additionalProperties": true + } + }, + "examples": { + "addAndRemove": { + "summary": "Add and remove", + "value": [ + { "op": "add", "path": "/score", "value": 42 }, + { "op": "remove", "path": "/oldKey" } + ] + }, + "appendToList": { + "summary": "Append to LiveList", + "value": [ + { "op": "add", "path": "/layers/-", "value": "newLayer" } + ] + } + } + } + } + }, + "responses": { + "200": { + "description": "Success. All operations were applied; the Storage document was updated." + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "description": "Patch failed. The document was not changed. The response body includes an error code and a helpful message (e.g. invalid path, test failed, index out of bounds).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "INVALID_STORAGE_MUTATION_REQUEST": { + "value": { + "error": "INVALID_STORAGE_MUTATION_REQUEST", + "message": "Test failed: value at path \"/name\" does not match.", + "suggestion": "Ensure the test value matches the current document value.", + "docs": "" + } + }, + "UNPROCESSABLE_ENTITY": { + "value": { + "error": "UNPROCESSABLE_ENTITY", + "message": "Invalid JSON pointer: path must start with \"/\".", + "suggestion": "Please ensure the data follows the correct format.", + "docs": "" + } + } + } + } + } + } + } + } + }, "/rooms/{roomId}/ydoc": { "get": { "summary": "Get Yjs document", diff --git a/guides/guides.json b/guides/guides.json index ae2277ea049..ee47b460e81 100644 --- a/guides/guides.json +++ b/guides/guides.json @@ -489,5 +489,12 @@ "topics": ["tutorials", "testing"], "technologies": ["nextjs", "react"], "date": "2026-02-17" + }, + { + "title": "Modifying Storage via REST API with JSON Patch", + "path": "/modifying-storage-via-rest-api-with-json-patch", + "topics": ["rest-api", "storage"], + "technologies": ["storage"], + "date": "2026-02-20" } ] diff --git a/guides/pages/modifying-storage-via-rest-api-with-json-patch.mdx b/guides/pages/modifying-storage-via-rest-api-with-json-patch.mdx new file mode 100644 index 00000000000..a817b170f2f --- /dev/null +++ b/guides/pages/modifying-storage-via-rest-api-with-json-patch.mdx @@ -0,0 +1,186 @@ +--- +meta: + title: "Modifying Storage via REST API with JSON Patch" + parentTitle: "Guides" + description: + "Use the JSON Patch HTTP API to modify Liveblocks Storage from any language." +--- + +The +[Storage JSON Patch endpoint](/docs/api-reference/rest-api-endpoints#patch-rooms-roomId-storage-json-patch) +allows you to apply partial updates to a room's +[Storage](/docs/ready-made-features/multiplayer/sync-engine/liveblocks-storage) +document over HTTP. You send an array of operations (add, remove, replace, move, +copy, test) and the server applies them in order. This is especially useful for +modifying Storage from languages that don't yet have a native Liveblocks client, +for example, such as Python, so you can still read and update Storage using the +REST API. + +## Format + +The format follows the +[JSON Patch](https://datatracker.ietf.org/doc/html/rfc6902) specification (RFC +6902). Paths use [JSON Pointer](https://datatracker.ietf.org/doc/html/rfc6901) +(RFC 6901). + +```shell title="Endpoint" +PATCH https://api.liveblocks.io/v2/rooms/{roomId}/storage/json-patch +``` + +Authenticate with your project's **secret key** in the `Authorization` header. + +```shell +"Authorization: Bearer {{SECRET_KEY}}" +``` + +Never expose your secret key in client-side code. + +## Liveblocks data types and paths + +Storage is a tree of Liveblocks types: +[LiveObject](/docs/api-reference/liveblocks-client#LiveObject), +[LiveList](/docs/api-reference/liveblocks-client#LiveList), and +[LiveMap](/docs/api-reference/liveblocks-client#LiveMap). When building your +patch: + +- **Be as specific as possible** with your target path. +- **Every parent** in the chain of path segments must be a LiveObject, LiveList, + or LiveMap. You cannot target or traverse through plain JSON objects that + aren't one of these types. +- **Nested values in `add` and `replace`:** If you pass a complex nested object + or array as the `value` for an `add` or `replace` operation, it is + automatically converted into LiveObjects and LiveLists on the server. + +## Operations and examples + +The request body is a JSON array of operation objects. Each object has an `op` +field and a `path` field; some operations also require `value` or `from`. + +### add + +Adds a value at the given path. For an object/key, creates or replaces the +member. For a list, inserts at the given index. + +- **Required:** `path`, `value` +- Use **`-`** as the last path segment to **append to the end of a LiveList** + (e.g. `"/layers/-"`). This is defined in RFC 6902. + +```json +[ + { "op": "add", "path": "/score", "value": 42 }, + { "op": "add", "path": "/layers/-", "value": "newLayer" } +] +``` + +### remove + +Removes the value at the given path. + +- **Required:** `path` + +```json +[{ "op": "remove", "path": "/oldKey" }] +``` + +### replace + +Replaces the value at the given path. The target must exist. + +- **Required:** `path`, `value` + +```json +[{ "op": "replace", "path": "/score", "value": 100 }] +``` + +### move + +Removes the value at `from` and adds it at `path`. The `from` location must +exist. Per RFC 6902, `from` must not be a prefix of `path` (you cannot move a +node into one of its children). + +- **Required:** `from`, `path` + +```json +[{ "op": "move", "from": "/name", "path": "/username" }] +``` + +### copy + +Copies the value at `from` to `path`. The `from` location must exist. + +- **Required:** `from`, `path` + +```json +[{ "op": "copy", "from": "/template", "path": "/current" }] +``` + +### test + +Checks that the value at `path` equals `value`. If it doesn't, the whole patch +fails and no changes are applied. Use `test` to guard against concurrent updates +(e.g. "only replace if the counter is still 5"). + +- **Required:** `path`, `value` + +```json +[ + { "op": "test", "path": "/version", "value": 1 }, + { "op": "replace", "path": "/version", "value": 2 } +] +``` + +## The test operation + +The **test** operation is a guard: it succeeds only when the value at the given +path is equal to the provided value (same type and value). If the test fails, +the server returns 422 and **does not apply any part of the patch**. This lets +you implement optimistic checks, for example, only increment a counter if it +still has the value you read earlier. + +_note:_ test order matters. A test after a change will take into the account the +changes made by prior operations. + +## Atomicity + +**If any operation in the patch fails, the entire document is left unchanged.** +Operations are applied in order; as soon as one fails (e.g. invalid path, test +failure, index out of bounds), the server stops and returns an error. No partial +updates are applied. + +## Error handling + +When a patch fails, the server responds with **422 Unprocessable Entity**. The +response body follows the standard API error shape and includes: + +- **error**: Error code (e.g. `INVALID_STORAGE_MUTATION_REQUEST`, + `UNPROCESSABLE_ENTITY`) +- **message**: Human-readable description of what went wrong +- **suggestion**: Optional hint on how to fix the request +- **docs**: Optional link to documentation + +Use these fields to show helpful feedback and retry or correct the request. + +## Performance and document size + +To apply a patch, the server must **reconstruct the full Storage state** for the +room. For large Storage documents, this can be expensive in time and resources. +The JSON Patch endpoint may not be suitable for very large documents; prefer the +native client SDKs when you need to update large or frequently changing Storage +from a supported environment. + +## Example: cURL + +```bash +curl -X PATCH "https://api.liveblocks.io/v2/rooms/my-room-id/storage/json-patch" \ + -H "Authorization: Bearer {{SECRET_KEY}}" \ + -H "Content-Type: application/json" \ + -d '[ + { "op": "test", "path": "/count", "value": 10 }, + { "op": "replace", "path": "/count", "value": 11 }, + { "op": "add", "path": "/log/-", "value": "updated" } + ]' +``` + +For the full request/response schema and more examples, see the +[API reference](/docs/api-reference/rest-api-endpoints) for the Storage JSON +Patch endpoint.