Real-time Co-editing with Instatic Yjs CRDT Synchronization: Implementation Guide

Instatic implements live collaborative editing by embedding Yjs CRDT documents into every logical content row, isolating the CRDT engine from application logic while keeping the editor store as the single source of truth.

Instatic, the open-source CMS from CoreBunch, enables multiple users to edit pages, components, and layouts simultaneously without conflicts. This article examines how the platform achieves real-time co-editing with Instatic Yjs CRDT synchronization, detailing the architecture that leverages conflict-free replicated data types to guarantee convergence across peers while maintaining a clean, type-safe domain model.

Architecture Overview

The collaboration layer treats every editable entity—pages, visual components, layouts, and the site-shell—as an isolated CRDT document. By embedding a Yjs Doc into each logical row, Instatic ensures that all changes converge regardless of network latency or editing order.

The system separates concerns into three distinct layers: persistent storage, transport validation, and client-side synchronization. This isolation prevents malformed updates from corrupting the database while allowing the UI to apply local changes synchronously.

Core Components

How the Collaboration Engine Works

The end-to-end flow relies on the CRDT guarantee of convergence: regardless of update order or timing, every replica ends with the same document state.

  1. Document creation – When a row is first created (e.g., a new page), the server instantiates a fresh Yjs Doc and stores its binary state via insertCollabDoc in server/repositories/collabDocuments.ts.

  2. Client connection – The admin UI opens a WebSocket to /_instatic/collab/:docId. The client provider attaches a WebsocketProvider from y-protocols to the Yjs Doc.

  3. State sync – Yjs exchanges state vectors to compute the minimal delta. Updates travel over the socket, are validated by updateGuard.ts, persisted, and broadcast to all peers.

  4. Local edits – The editor store mutates the Y.Map or Y.Text directly. Because Yjs updates apply synchronously inside the same event loop, the UI reflects changes instantly.

  5. Caret broadcast – When a user moves the cursor, the current relative position encodes into a small Yjs update via awareness.setLocalStateField; remote UI components render it using caretPositions.ts.

  6. Conflict resolution – When a remote splice arrives that cannot map one-to-one (e.g., local divergence), inlineEditRemoteMerge.ts synthesizes a compatible splice, ensuring the merged text remains valid.

Implementation Examples

Initializing the Client Provider

The createCollabProvider function in src/admin/pages/site/collab/collabProvider.ts wires the WebSocket transport to the Yjs document:

import { createCollabProvider } from '@/admin/pages/site/collab/collabProvider';

function useCollab(docId: string) {
  const provider = useMemo(() => createCollabProvider(docId), [docId]);
  // `provider.doc` is the Yjs Doc; attach Y.Map/Y.Text structures here.
  return provider;
}

This registers listeners for update and awareness messages, returning an object containing the live doc and an awareness instance for cursor tracking.

Persisting Documents on the Server

When creating a new page, the server initializes the CRDT state and persists the binary blob:

import { insertCollabDoc } from '@/server/repositories/collabDocuments';
import * as Y from 'yjs';

export async function createPageCollab(pageId: string) {
  const ydoc = new Y.Doc();
  // Insert a top-level map for the page’s content.
  ydoc.getMap('content');
  const binary = Y.encodeStateAsUpdate(ydoc);
  await insertCollabDoc(`page:${pageId}`, binary);
}

Future connections for the same page:<id> load this blob via server/repositories/collabDocuments.ts and resume the CRDT from the saved state vector.

Broadcasting Cursor Positions

Live cursors rely on the Yjs awareness protocol:

import { awareness } from '@/admin/pages/site/collab/collabProvider';

function broadcastCaret(anchor: number, head: number) {
  awareness.setLocalStateField('caret', { anchor, head });
}

The awareness object automatically diff-sends the payload to all peers. Remote clients decode these positions using the caretPositions.ts utilities to render floating carets at the correct indices.

Validating Inbound Updates

The server guards against corruption using verifyYUpdate in server/collab/updateGuard.ts:

import { verifyYUpdate } from '@/server/collab/updateGuard';

export async function handleCollabUpdate(req) {
  const { docId, update } = await req.json();
  // Throws if the binary update does not decode to a valid Yjs Update.
  verifyYUpdate(update);
  await appendCollabUpdate(docId, update);
}

This function decodes the binary packet with Y.applyUpdate on a temporary document; any exception rejects the update, protecting the persisted blob from invalid data.

Key Files Reference

File Purpose
src/core/collab/index.ts Public entry point describing the collab engine and exporting core utilities.
src/core/collab/docIds.ts Functions mapping CMS entities to deterministic Yjs document IDs.
src/core/collab/integrity.ts Runtime checks ensuring Yjs state cannot produce invalid published HTML.
src/admin/pages/site/collab/collabProvider.ts Client-side setup for WebsocketProvider, Doc attachment, and awareness management.
src/admin/pages/site/collab/caretPositions.ts Encoding/decoding of remote cursor positions using Yjs relative positions.
src/admin/pages/site/collab/inlineEditRemoteMerge.ts Logic merging remote text edits into the local editor’s delta stream.
server/collab/socket.ts WebSocket endpoint streaming Yjs updates between server and browsers.
server/collab/updateGuard.ts Validation layer sanitizing inbound Yjs updates before persistence.
server/repositories/collabDocuments.ts Persistence API for binary Yjs state blobs (one per document).

Summary

  • Isolated CRDT documents – Each content row (page, component, layout) maintains its own Yjs Doc, ensuring failure isolation and clean domain boundaries.
  • Atomic WebSocket relay – The server/collab/socket.ts endpoint handles updates atomically, echoing changes back to originators so the UI confirms receipt.
  • Defensive validationupdateGuard.ts rejects malformed binary updates before they reach the database, preventing corruption of the collab state.
  • Synchronous local updates – The editor store mutates Yjs types directly, providing instant feedback while the CRDT handles convergence asynchronously.
  • Relative position cursors – Caret locations broadcast as Yjs relative positions (anchor/head), surviving concurrent text insertions and deletions.
  • Conflict synthesisinlineEditRemoteMerge.ts generates compatible splices when remote and local deltas diverge, preserving text validity.

Frequently Asked Questions

What is the role of Yjs in Instatic's collaboration system?

Yjs provides the CRDT engine that guarantees all replicas converge to the same state regardless of update order. Instatic embeds a Yjs Doc into each content row and uses Yjs's binary update format for network transport, leveraging the library's automatic conflict resolution for concurrent text edits.

How does Instatic prevent corrupted data from reaching the database?

The server/collab/updateGuard.ts module validates every incoming binary update using verifyYUpdate, which attempts to apply the update to a temporary Yjs document. If decoding throws an exception, the server rejects the update before it reaches server/repositories/collabDocuments.ts, protecting the persisted blob from corruption.

How are document IDs structured in the collaboration layer?

Document IDs follow deterministic patterns defined in src/core/collab/docIds.ts, such as page:<id>, component:<id>, layout:<id>, and site. These IDs route WebSocket connections to the correct persisted blob and ensure the client and server reference the same CRDT document.

What happens when two users edit the same text simultaneously?

When remote edits arrive that cannot map directly to the local document state (due to local divergence), src/admin/pages/site/collab/inlineEditRemoteMerge.ts transforms the remote splice into a compatible Y.TextEvent delta. This synthesis ensures the final merged text remains valid and consistent across all peers, adhering to the CRDT convergence guarantee.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →