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
Here's the repo! lblcks.io/XSVJq9F
github.com
GitHub - CTNicholas/multiplayer-coding-agents
Contribute to CTNicholas/multiplayer-coding-agents development by creating an account on GitHub.
020
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
Here's the repo! lblcks.io/lvQR2TQ
github.com
GitHub - CTNicholas/jev-workflow-builder
Contribute to CTNicholas/jev-workflow-builder development by creating an account on GitHub.
020
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
This auto-fill feature already existed in my app, but it took 5+ seconds to complete using LLMs. Now it takes 100ms!
010
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
Learn more about Sync! lblcks.io/xeJ54dM
lblcks.io
Liveblocks Sync | The sync engine for the agentic web
Add collaborative editing for humans and agents, without building infrastructure.
010
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 · 16/09/2026
Adding real-time collaboration? Work at a startup? @liveblocks.io offers discounts for early-stage companies, give us a message and we'll do our best to help you out! lblcks.io/h9IcT7O
lblcks.io
Build Multiplayer Products from Day One | Liveblocks for Startups
The best products are realtime and multiplayer from day one. Liveblocks is the sync engine that lets startups ship collaborative, AI-ready products in days, free to start.
010
Chris Nicholas @chrisnicholas.dev · 10/09/2026
All credit goes to @marcbouchenoire.com! Check out his thread. bsky.app/profile/mar...
bsky.app
Marc Bouchenoire (@marcbouchenoire.com)
We made a new liveblocks.io 3D hero 💧
010
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 · 10/09/2026
Hero built by @marcbouchenoire.com! Excellent job, as always.
010
Reposted by Chris Nicholas
Liveblocks @liveblocks.io · 10/09/2026
We've shipped a new hero. → lblcks.io/WJGKiD9
221
Reposted by Chris Nicholas
Marc Bouchenoire @marcbouchenoire.com · 10/09/2026
We made a new liveblocks.io 3D hero 💧
242
Chris Nicholas @chrisnicholas.dev · 10/09/2026
Incredible work! 👏
020
Chris Nicholas @chrisnicholas.dev · 04/09/2026
LiveText is in public beta today. Learn more ↓ lblcks.io/livetext
lblcks.io
LiveText: a new data type for collaborative text editing | Liveblocks blog
Introducing LiveText, a new primitive for adding realtime multiplayer to rich-text editors. An alternative to Yjs, it supports Tiptap, BlockNote, ProseMirror, and CodeMirror.
040
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
Reposted by Chris Nicholas
Liveblocks @liveblocks.io · 01/09/2026
Meet Liveblocks Sync—the sync engine for the agentic web. Add collaborative editing for humans and agents, without building infrastructure. lblcks.io/sync
lblcks.io
Liveblocks Sync | The sync engine for the agentic web
Add collaborative editing for humans and agents, without building infrastructure.
131
Chris Nicholas @chrisnicholas.dev · 01/09/2026
We have get started guides and packages for all sorts of use cases, such as flowcharts, canvases, data grids, chats, text editors, and more. Learn more in our in-depth blog post! lblcks.io/sync-blog
lblcks.io
Introducing Liveblocks Sync: the sync engine for the agentic web | Liveblocks blog
Liveblocks Sync is our sync engine for the agentic web, enabling you to build multiplayer applications where humans, agents, or both, can edit documents together in realtime. Previously known as Storage, it now includes LiveText, a new alternative for Yjs.
010
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
Reposted by Chris Nicholas
Liveblocks @liveblocks.io · 01/09/2026
Introducing LiveText, part of Liveblocks Sync. Our new data type for collaborating text editing, and a replacement for Yjs. → lblcks.io/livetext
142
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