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

> Implement real-time co-editing with Instatic Yjs CRDT synchronization. Discover how Instatic embeds Yjs CRDT documents for seamless live collaboration.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: how-to-guide
- Published: 2026-08-02

---

**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

- **Collab document storage** ([`server/repositories/collabDocuments.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/repositories/collabDocuments.ts)) – Persists one Yjs state blob per row, enabling document resurrection on new connections.
- **CRDT update guard** ([`server/collab/updateGuard.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/collab/updateGuard.ts)) – Validates incoming binary updates before they reach the database, rejecting malformed packets.
- **WebSocket relay** ([`server/collab/socket.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/collab/socket.ts)) – Receives Yjs updates from browsers, writes them atomically, and echoes them back to the originator.
- **Document addressing** ([`src/core/collab/docIds.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/collab/docIds.ts)) – Generates deterministic IDs such as `page:<id>`, `component:<id>`, `layout:<id>`, and `site` to route updates correctly.
- **Client-side provider** ([`src/admin/pages/site/collab/collabProvider.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/site/collab/collabProvider.ts)) – Initializes the Yjs `Doc`, attaches a `WebsocketProvider`, and wires the editor store.
- **Caret synchronization** ([`src/admin/pages/site/collab/caretPositions.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/site/collab/caretPositions.ts)) – Encodes cursor positions as Yjs relative positions (anchor + head) for live remote cursors.
- **Inline edit conflict resolution** ([`src/admin/pages/site/collab/inlineEditRemoteMerge.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/site/collab/inlineEditRemoteMerge.ts)) – Transforms remote splices into local `Y.TextEvent` deltas when documents have diverged.
- **Integrity checks** ([`src/core/collab/integrity.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/collab/integrity.ts)) – Validates that Yjs-generated structures cannot produce invalid published HTML.

## 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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/caretPositions.ts).

6. **Conflict resolution** – When a remote splice arrives that cannot map one-to-one (e.g., local divergence), [`inlineEditRemoteMerge.ts`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/site/collab/collabProvider.ts) wires the WebSocket transport to the Yjs document:

```tsx
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:

```ts
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`](https://github.com/CoreBunch/Instatic/blob/main/server/repositories/collabDocuments.ts) and resume the CRDT from the saved state vector.

### Broadcasting Cursor Positions

Live cursors rely on the Yjs awareness protocol:

```ts
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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/server/collab/updateGuard.ts):

```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`](https://github.com/CoreBunch/Instatic/blob/main/src/core/collab/index.ts) | Public entry point describing the collab engine and exporting core utilities. |
| [`src/core/collab/docIds.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/collab/docIds.ts) | Functions mapping CMS entities to deterministic Yjs document IDs. |
| [`src/core/collab/integrity.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/collab/integrity.ts) | Runtime checks ensuring Yjs state cannot produce invalid published HTML. |
| [`src/admin/pages/site/collab/collabProvider.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/site/collab/collabProvider.ts) | Client-side setup for `WebsocketProvider`, `Doc` attachment, and awareness management. |
| [`src/admin/pages/site/collab/caretPositions.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/site/collab/caretPositions.ts) | Encoding/decoding of remote cursor positions using Yjs relative positions. |
| [`src/admin/pages/site/collab/inlineEditRemoteMerge.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/site/collab/inlineEditRemoteMerge.ts) | Logic merging remote text edits into the local editor’s delta stream. |
| [`server/collab/socket.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/collab/socket.ts) | WebSocket endpoint streaming Yjs updates between server and browsers. |
| [`server/collab/updateGuard.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/collab/updateGuard.ts) | Validation layer sanitizing inbound Yjs updates before persistence. |
| [`server/repositories/collabDocuments.ts`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/server/collab/socket.ts) endpoint handles updates atomically, echoing changes back to originators so the UI confirms receipt.
- **Defensive validation** – [`updateGuard.ts`](https://github.com/CoreBunch/Instatic/blob/main/updateGuard.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 synthesis** – [`inlineEditRemoteMerge.ts`](https://github.com/CoreBunch/Instatic/blob/main/inlineEditRemoteMerge.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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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.