How to modify Liveblocks Storage from the server

In realtime applications, Liveblocks Storage is generally modified from the browser with useMutation or through conflict-free data methods. However, sometimes it can be useful to modify your realtime storage from server-side Node.js too.

What we’re building

In this guide, we’ll be building a function that allows you to easily modify storage from the server. We’ll do this by running @liveblocks/client server-side using Node.js polyfills, and by signing in with a service account userId.

await modifyStorage("my-room-name", (root) => {  root.get("list").push("item3");});

Set up Liveblocks server config

The first thing we need to do is to install the required Node.js polyfills.

$npm i node-fetch ws

After this we can create a server config file, which we’ll name liveblocks.server.config.ts. In this file we’re implementing the following.

  1. Creating a node client with new Liveblocks.
  2. Creating a regular client to be used on the server, serverClient.

  3. Authenticating inside the regular client.
  4. Using the same userId for server changes, so that MAUs do not increase.

  5. Adding Node.js polyfills to the regular client.
  6. Creating a typed enter room function.

Here’s the full file:

liveblocks.server.config.ts
import { createClient } from "@liveblocks/client";import type {  Presence,  Storage,  UserMeta,  RoomEvent,} from "./liveblocks.config";import { Liveblocks } from "@liveblocks/node";import fetch from "node-fetch";import WebSocket from "ws";
// 1. Creating a node clientconst liveblocks = new Liveblocks({ secret: "",});
// 2. Creating a regular clientexport const serverClient = createClient({ // 3. Authenticating inside the client authEndpoint: async (room) => { const session = liveblocks.prepareSession( // 4. Using a specific userId for all server changes "_SERVICE_ACCOUNT" ); session.allow(room, session.FULL_ACCESS); const { body } = await session.authorize(); return JSON.parse(body); },
// 5. Adding polyfills polyfills: { fetch: fetch as any, WebSocket, },});
// 6. Creating a typed enter room functionexport const enterRoom = (roomId: string) => { return serverClient.enter<Presence, Storage, UserMeta, RoomEvent>(roomId, { // Match the options in your browser code });};

Create the modify storage function

Using serverClient and enterRoom from the previous file, we can create a typed modifyStorage function that allows us to join a room, modify storage (batching all changes into one request), before leaving the room.

import type { LiveObject } from "@liveblocks/client";import type { Storage } from "./liveblocks.config";import { serverClient, enterRoom } from "./liveblocks.server.config";
export async function modifyStorage( roomId: string, storageChanges: (root: LiveObject<Storage>) => void) { const roomContext = enterRoom(roomId); const { room } = roomContext; const { root } = await room.getStorage();
// Make storage adjustments in a batch, so they all happen at once room.batch(() => { storageChanges(root); });
// If storage changes are not synchronized, wait for them to finish if (room.getStorageStatus() !== "synchronized") { await room.events.storageStatus.waitUntil( (status) => status === "synchronized" ); }
// Leave when storage has been synchronized roomContext.leave(roomId);}

Start modifying storage

We can now start modify storage from the server! Import modifyStorage, pass a room name, and use the callback to modify as you like.

route.ts
import { modifyStorage } from "./modifyStorage";
export async function POST() { console.log("Updating storage");
await modifyStorage("my-liveblocks-room", (root) => { root.get("list").push("item3"); });
console.log("Storage update complete!");}

Account for the service user in your app

Remember to account for the service user appearing in your presence. In our liveblocks.server.config.ts we authenticated with "_SERVICE_ACCOUNT" as the userId, so we’ll filter it out when using others in our application.

import { shallow } from "@liveblocks/react";import { useOthers } from "./liveblocks.config.ts";
function LiveAvatars() { // Others, with the service account filtered out const others = useOthers( (others) => others.filter((other) => other.id !== "_SERVICE_ACCOUNT"), shallow );
// ...}

A shallow equality check is necessary here, because filter creates a new array every time.