Sign in

Chris Nicholas

@chrisnicholas.dev
283 followers 294 following 558 posts

Developer experience @liveblocks.io • Interactive articles on chrisnicholas.dev

PostsRepliesMedia
Chris Nicholas @chrisnicholas.dev · 24/09/2026
There's some other features too! Add skills by putting markdown files into the /skills folder. Plus, if you're not in the chat, you get a notification when an agent has finished coding.
120
Chris Nicholas @chrisnicholas.dev · 24/09/2026
Agents use Cursors's fantastic APIs to make changes to your repo. They spin up a virtual environment and can create PRs for you, very simple. And the diff in the sidebar uses diffs.com to show the code changes!
120
Chris Nicholas @chrisnicholas.dev · 24/09/2026
It's not just for coding, you can also generate markdown documents together! Plus, after the agent's created a file, you can hop in and edit it in real-time with others. The multiplayer chat and editor are both powered by @liveblocks.io!
120
Chris Nicholas @chrisnicholas.dev · 24/09/2026
I've built an open-source coding agent tool! Jev classifies every message—the chats are multiplayer, and humans can talk normally, but if Jev recognises a coding request or question, it hops in and triggers the correct agent. Get it + info ↓
121
Chris Nicholas @chrisnicholas.dev · 21/09/2026
And of course it's a multiplayer editor! It uses the React Flow package for @liveblocks.io, which automatically syncs state between multiple users.
120
Chris Nicholas @chrisnicholas.dev · 21/09/2026
Check through previous runs, see what happened, and run them again with your updated nodes.
120
Chris Nicholas @chrisnicholas.dev · 21/09/2026
Each workflow exposes a REST API which you can call in your own code. Here's a cURL example in the terminal!
120
Chris Nicholas @chrisnicholas.dev · 21/09/2026
I've built an open-source workflow builder for Jev! Experiment with a UI—classify inputs using choices, scores, booleans; wire results into other prompts; and return a set of custom outputs. Below I've set up a support system that can help with various queries. Features ↓
121
Chris Nicholas @chrisnicholas.dev · 18/09/2026
It's actually multiplayer here too—it uses liveblocks.io/sync to sync document state and presence. As soon as its results are returned, I update the document with the new values, from the server.
111
Chris Nicholas @chrisnicholas.dev · 18/09/2026
I love that Jev can fit into basically any app. You don't need to build everything around a chat, you can just integrate intelligent features into the background, like these auto-fill buttons. Much better UX than with slow LLMs. Feels like the missing piece!
110
Chris Nicholas @chrisnicholas.dev · 17/09/2026
And if you know me already, it's multiplayer of course! It works brilliantly with @liveblocks.io Sync, which allows you to store realtime document and agent state. Having such a quick AI makes it feel almost local.
110
Chris Nicholas @chrisnicholas.dev · 17/09/2026
Combine it with an LLM and you can use it to diagnose issues, come up with solutions, and create "Fix it" buttons. This way you can avoid running your expensive model on every change.
100
Chris Nicholas @chrisnicholas.dev · 17/09/2026
Jev enables instant AI suggestions 👏 In this app, 20+ intelligent checks occur every time you edit a cell, immediately providing warnings. No LLMs necessary!
121
Chris Nicholas @chrisnicholas.dev · 10/09/2026
Our homepage now has a multiplayer game! ✦ Real-time cursors & physics. ✦ Ripples & web page reflections. ✦ Secret code unlocks the easter egg. → lblcks.io/WJGKiD9
120
Chris Nicholas @chrisnicholas.dev · 04/09/2026
And unlike Yjs, there are no tombstones, so documents never grow over time. A doc edited daily for years loads as fast as one written yesterday. Your text is plain, readable data too, with no binary types to handle.
import type { LiveTextData } from "@liveblocks/client";

// Plain text
const plain: LiveTextData = [["Hello world"]];

// Text with inline formatting
const formatted: LiveTextData = [["Hello ", { bold: true }], ["world"]];
130
Chris Nicholas @chrisnicholas.dev · 04/09/2026
You can even edit text from the server, so your AI agents can write into documents in realtime. Use `mutateStorage` in Node.js, or JSON Patch operations via REST API.
const { text } = await generateText({
  model: openai("gpt-5.6-sol"),
  prompt: `Write a document about realtime collaboration`,
});

await liveblocks.mutateStorage("my-room-id", ({ root }) => {
  const content = root.get("document").get("content");
  content.insert(0, text);
});
130
Chris Nicholas @chrisnicholas.dev · 04/09/2026
Because LiveText is a conflict-free type like LiveObject and LiveList, you can use it directly in your Storage tree too. You can create a whole multiplayer data tree, and nest your LiveText content fields inside it.
// Creating LiveText content
const content = new LiveText([["Hello world"]]);

// Creating a custom document
const document = new LiveObject({
  title: "Untitled",
  content,
});

// Updating the content
content.insert(0, "This week, I…", { format: "italic" });
130
Chris Nicholas @chrisnicholas.dev · 04/09/2026
Tiptap, BlockNote, ProseMirror, and CodeMirror are supported at launch, and more may come later. Each editor just needs a simple extension to add collaboration, like Tiptap below, and you can even render multiple documents in the same room.
export function Editor() {
  const liveblocks = useLiveblocksExtension({
    collaborationMode: "liveblocks",
  });

  const editor = useEditor({
    extensions: [
      liveblocks,
      StarterKit.configure({ undoRedo: false }),
    ],
  });

  return <EditorContent editor={editor} />;
}
130
Chris Nicholas @chrisnicholas.dev · 04/09/2026
We shipped a Yjs alternative for collaborative text editing! LiveText is a new data type for @liveblocks.io. It's quicker, simpler, and smaller than a CRDT, and integrates into four popular text editors. And there's more ↓
171
Chris Nicholas @chrisnicholas.dev · 01/09/2026
Code editors are supported too! For example, here's the popular CodeMirror editor.
110
Chris Nicholas @chrisnicholas.dev · 01/09/2026
And speaking of Google Docs, you can enable real-time text editing too. There's two ways to do this, Yjs, and LiveText, both with their advantages. In a few lines of code, add it to your editor, such as Tiptap or BlockNote.
110
Chris Nicholas @chrisnicholas.dev · 01/09/2026
Permissions are built in, allowing you to control which users, and groups of users, should have access to the document. Use REST APIs to create share dialogs like you'd find in Notion or Google Docs.
110
Chris Nicholas @chrisnicholas.dev · 01/09/2026
Additionally, Storage comes with ready-made undo and redo hooks, making it trivial to undo changes. Crucially, pressing undo only reverts your own changes, and not changes made by others.
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>
    </>
  );
}
110
Chris Nicholas @chrisnicholas.dev · 01/09/2026
Your Storage data comes with version history, allowing you to revert documents to their previous state. I'd recommend creating a new snapshot before applying changes made by AI, but you can also set it up to periodically save state.
function VersionsSidebar() {
  const [selectedVersionId, setSelectedVersionId] = useState();
  const { versions } = useHistoryVersions();
  const restore = useRestoreToStorageVersion(versionId);

  return (
    <HistoryVersionSummaryList>
      {versions.map((version) => (
        <HistoryVersionSummary
          key={version.id}
          version={version}
          selected={version.id === selectedVersionId}
          onClick={() => setSelectedVersionId(version.id)}
        />
      ))}
      <button onClick={() => restore()}>↩️ Restore</button>
    </HistoryVersionSummaryList>
  );
}


// Create a manual snapshot from the server
await liveblocks.createVersionHistorySnapshot("my-room-id");
110
Chris Nicholas @chrisnicholas.dev · 01/09/2026
On the server, you can create a new feed message, then render in each chunk as it's generated. In React, you'll see the message stream in for all connected users in real-time. Refresh the page whilst streaming, and it'll all still work.
"use server";

export async function generateResponse(roomId, feedId, messages) {
  const messageId = crypto.randomUUID();

  await liveblocks.createFeedMessage({
    roomId,
    feedId,
    messageId,
    data: { role: "assistant", content: text },
  });

  const { textStream } = streamText({
    model: "openai/gpt-5.6-sol",
    system: "You are a helpful assistant in a team chat.",
    messages,
  });

  let content = "";

  for await (const chunk of textStream) {
    content += chunk;
    await liveblocks.updateFeedMessage({
      roomId,
      feedId,
      messageId,
      data: { role: "assistant", content },
    });
  }
}
110
Chris Nicholas @chrisnicholas.dev · 01/09/2026
Here's one way to hook `useFeedMessages` into a chat app. You can quite easily do it with any existing UI library.
import { generateResponse } from "./generate-response";

function Chat({ feedId }: { feedId: string }) {
  const { messages } = useFeedMessages(feedId);
  const createFeedMessage = useCreateFeedMessage();

  return (
    <>
      {messages.map((message) => (
        <Message key={message.id} from={message.data.role}>
          {message.data.content}
        </Message>
      ))}
      <PromptInput
        onSubmit={(message) => {
          createFeedMessage(feedId, {
            role: "user",
            content: message,
          });
          generateResponse(roomId, feedId, messages);
        }}
      />
    </>
  );
}
110
Chris Nicholas @chrisnicholas.dev · 01/09/2026
Feeds allows you to create paginated streams of data, ideal for creating multiplayer chats, storing workflow data, or sharing an agent's status in your UI.
110
Chris Nicholas @chrisnicholas.dev · 01/09/2026
For example, with a spreadsheet, you might generate a new cell value, and then apply it to the document. Running the following code will update the spreadsheet in real-time for all connected users.
await liveblocks.mutateStorage("my-room-id", ({ root }) => {
  const cells = root.get("cells");

  // Generate an AI response
  const { text: value } = await generateText({
    model: "openai/gpt-5.6-sol",
    prompt: `Fill in cell B2. Current cells: ${cells.toJSON()}`,
  });

  // Update the cell with the AI response
  cells.set("B2", value);
});
110
Chris Nicholas @chrisnicholas.dev · 01/09/2026
Agents can collaborate too, using server-side methods like `mutateStorage`. Trigger these from chats, buttons, comments, or wherever you choose. AI can show its presence too, as it makes changes, and it'll appear in `useOthers`.
120
Chris Nicholas @chrisnicholas.dev · 01/09/2026
More complex presence need `useUpdateMyPresence`, allowing you to attach live JSON values to users. For example, you can add an outline to a `textarea`, when a user selects it, by giving each user a `selection: string | null` value, representing the box they're editing.
function Textarea({ id }: { id: string }) {
  const updateMyPresence = useUpdateMyPresence();
  const useOthers = useOthers();

  // Is anyone selecting this textarea `id`?
  const isSelected = others.find(
    ({ presence }) => presence.selection === id
  );

  return (
    <textarea
      id={id}
      // Update presence if they select or de-select
      onSelect={() => updateMyPresence({ selection: id })}
      onBlur={() => updateMyPresence({ selection: null })}
      style={{
        outline: isSelected ? "2px solid blue" : "none",
      }}
    />
  );
}
220
Chris Nicholas @chrisnicholas.dev · 01/09/2026
`useOthers` allows you to show other users presence in real-time, for example this is how to create a live avatar stack.
function AvatarStack() {
  // Get an array of every other connected user
  const others = useOthers();

  // Render an avatar stack
  return (
    <div>
      {others.map(({ connectionId, info }) => (
        <img key={connectionId} src={info.avatar} />
      ))}
    </div>
  );
}
110
Chris Nicholas @chrisnicholas.dev · 01/09/2026
Another feature is presence. Connected users can show their activity, like with cursors, selections around elements, or an avatar in a stack.
110
Chris Nicholas @chrisnicholas.dev · 01/09/2026
In React, you can read and edit Storage values with two hooks. 1. `useStorage`: Return a plain JSON representation of your data. 2. `useMutation`: Edit your data with live structures. Combining these allows you to, for example, render your slides, and add a "New slide" button.
function Slides() {
  // Returns a JS array of the slides
  const slides = useStorage((root) => root.slides);

  // Adds a new slide LiveObject to the array
  const addSlide = useMutation(({ storage }) => {
    const newSlide = new LiveObject({
      title: "Untitled",
      content: new LiveText(["<h1>Welcome!</h1>"]),
    });

    storage.get("slides").push(newSlide);
  }, []);

  return (
    <div>
      {slides.map((slide) => (
        <Slide key={slide.id} slide={slide} />
      ))}
      <button onClick={addSlide}>➕ Add Slide</button>
    </div>
  );
}
110
Chris Nicholas @chrisnicholas.dev · 01/09/2026
Storage is the main building block, a primitive for storing multiplayer state. It comes with conflict-free data types such as `LiveObject`, which is similar to a JS object that multiple users can edit simultaneously For example, here's how a slideshow app might be constructed.
import { LiveObject, LiveList, LiveText } from "@liveb…";

// Create a list of slides
const slides = new LiveList([]);

// Create a new slide
const newSlide = new LiveObject({
  title: "Untitled",
  content: new LiveText(["<h1>Welcome!</h1>"]),
});

// Add the new slide to the list=
slides.push(newSlide);
110
Chris Nicholas @chrisnicholas.dev · 01/09/2026
Liveblocks Sync is the sync engine designed for the agentic web. Add conflict-resolved multiplayer to any React app, so that humans and agents can edit the same state, at the same time. lblcks.io/sync Here's how it works ↓
131
Chris Nicholas @chrisnicholas.dev · 07/08/2026
diffs.com/edit is a fantastic new code editor, built by Pierre Computer Company. It's not just Monaco or ProseMirror, but a new editor built from scratch. Couldn't resist adding collaboration with @liveblocks.io!
021
Chris Nicholas @chrisnicholas.dev · 04/08/2026
LiveFile allows you to upload documents directly to the sync engine itself, without relying on a third-party service such as S3. Here's an example of it hooked up to a multiplayer whiteboard.
140
Chris Nicholas @chrisnicholas.dev · 04/08/2026
Liveblocks Storage now has version history! Snapshot your room's sync engine data, browse previous versions, preview earlier states, and restore any you need. Snapshots can be saved automatically at intervals, or created manually. We've released a new data type too ↓
251
Chris Nicholas @chrisnicholas.dev · 27/07/2026
You can also upload files from the server, handy if you're doing any back-end processing or AI generation. Call the server-side `mutateStorage` method, and each browser will update in real-time with the image.
121
Chris Nicholas @chrisnicholas.dev · 27/07/2026
It's quite simple, there's a new `useUploadFile` hook for uploading a file to a room, then attach the file to your Storage tree. Then fetch your file, and get its URL. You can create a download button, or if it's browser compatible (like an image), render it on the page.
import { useFileUrl, useStorage } from "@liveblocks/react/suspense";

function DownloadFile() {
  const myFile = useStorage((root) => root.myFile);
  const fileUrl = useFileUrl(myFile);

  return (
    <a href={fileUrl} download>
      ⬇️ Download file
    </a>
  );
}
120
Chris Nicholas @chrisnicholas.dev · 27/07/2026
You can now upload files to @liveblocks.io! If your multiplayer app uses media, archives, or any file, `LiveFile` allows you to upload them directly to your Storage tree. Other users will see the file appear in realtime—like in this @tldraw.com clip. Learn more ↓
143
Chris Nicholas @chrisnicholas.dev · 23/07/2026
Article: What's the best vector database for AI and RAG? Turbopuffer, Pinecone, pgvector, more—a deep-dive into the options. Our team's well positioned to give guidance, as we tried them before building our product. Written last year, we stand by our choice! Link ↓
120
Chris Nicholas @chrisnicholas.dev · 15/07/2026
Build a collaborative agentic workflow builder. In CodeWithAntonio's latest video, he explains how to build a browser automation tool—in the video you it automatically loads a news website, extracts headlines, and sends an email. Video tutorial ↓
231
Chris Nicholas @chrisnicholas.dev · 10/07/2026
When rendering your threads, check for `thread.visibility` to render private threads differently, like in the video above.
JavaScript code snippet for a component that maps threads and conditionally displays a message for private threads.
120
Chris Nicholas @chrisnicholas.dev · 10/07/2026
…the easiest way is to add a group to your room and remove their private comments access, like I'm doing here with `reader`. Then pass the `reader` `groupId` to a user when authenticating, and they won't be able to view private comments!
Code snippets illustrate managing user access in a room, emphasizing the "reader" group's inability to view private comments.
120
Chris Nicholas @chrisnicholas.dev · 10/07/2026
To create a private thread, pass `visibility: "private"` to `Composer` or `useCreateThread`. To access who can see it, there's a couple of ways to do it…
Code snippet shows a React component for a private comment composer, emphasizing private visibility settings for user comments.
120
Chris Nicholas @chrisnicholas.dev · 10/07/2026
Using @liveblocks.io Comments? You can now create private comment threads within a room. Set permissions to control exactly who can see and reply—ideal for internal notes and hidden feedback. How to set it up ↓
121
Chris Nicholas @chrisnicholas.dev · 08/07/2026
I've updated my open-source AI slideshow template! It's got a bunch of new features: ✦ Drag-and-drop elements, double-click to edit text. ✦ Editing is collaborative, users have presence. ✦ Multiplayer undo/redo, in both views. ✦ Create multiple slides. Get it ↓
131
Chris Nicholas @chrisnicholas.dev · 06/07/2026
Exporting to powerpoint currently generates a high-res image embedded into a powerpoint file. It looks pixel perfect, as you can see in Google Slides below, but you can't currently edit the text.
120
Chris Nicholas @chrisnicholas.dev · 06/07/2026
Commenting uses @liveblocks.io pre-made React components. You can leave reactions, mention others, and even create a notification inbox for missed comments. Threads are attached to the canvas using a percentage value from the top left, which works well with fixed slide ratio.
120