Plate
PlateEditorsTemplates
GitHub16kGitHub
DiscordDiscord
    • Overview
    • Why This Fork
    • Examples
    • Installing Plite
    • Adding Event Handlers
    • Defining Custom Elements
    • Applying Custom Formatting
    • Executing Commands
    • Saving to a Database
    • Canonical Change Substrate
    • Improving Performance
    • Interfaces
    • Nodes
    • Locations
    • Transforms
    • Document Changes
    • Commands
    • Editor
    • Plugins
    • Rendering
    • Serializing
    • Normalizing
    • Using TypeScript
    • Roots
    • Document State
    • Editing Behavior
    • Selection And DOM
    • Clipboard And Paste
    • Decorations, annotations, and widgets
    • Schema
    • Anchor API
    • Location API
    • Path API
    • PointEntry API
    • Point API
    • Range API
    • Selection API
    • Location Types APIs
    • Span API
    • Editor
    • Element API
    • NodeEntry API
    • Node API
    • Node Types APIs
    • Text API
    • Debug Value Scrubbing
    • Transforms API
    • Plite DOM
    • History Editor API
    • History Plugin Setup
    • History
    • Plite History
    • Plite Hyperscript
    • Plite PaginationExperimental
    • Annotations
    • DOM Coverage Boundaries
    • Editable Component
    • External Text Views
    • Plite React Event Handling
    • Virtualized Rendering
    • Plite React Hooks
    • React Editor Setup
    • React Editor
    • Plite React
    • Plite Component
    • Plite Yjs
    • Plite
    • Migration
    • Contributing
    • FAQ
    • Resources

Canonical Change Substrate

PreviousNext

Use commits, document changes, effects, and tags as the adapter boundary.

Remote editing in Plite starts with committed DocumentChange values. Plite does not choose the network layer, CRDT, persistence model, or awareness protocol. Adapter packages translate between that canonical change law and their distributed representation.

Commit a local edit

import { createEditor, decodeEditorEffect, encodeEditorEffect } from "plitejs";
 
const editor = createEditor({
  initialValue: [{ type: "paragraph", children: [{ text: "" }] }],
});
 
editor.update({ tags: ["local-edit", "collab-export"] }, (tx) 







Saving to a DatabaseImproving Performance

On This Page

Commit a local editExport a canonical changeImport a canonical changeSubscribe to commitsCommit shapeRuntime ids are localAdapter ownership
Build your editor
Production-ready AI template and reusable components.
Get all-access
=>
{
tx.text.insert("!");
tx.nodes.insert(
{ type: "paragraph", children: [{ text: "four" }] },
{ at: [1] }
);
});
const commit = editor.read.lastCommit();
import { createEditor, decodeEditorEffect, encodeEditorEffect } from "plitejs";
 
const editor = createEditor({
  initialValue: [{ type: "paragraph", children: [{ text: "" }] }],
});
 
editor.update({ tags: ["local-edit", "collab-export"] }, (tx) => {
  tx.text.insert("!");
  tx.nodes.insert(
    { type: "paragraph", children: [{ text: "four" }] },
    { at: [1] }
  );
});
 
const commit = editor.read.lastCommit();

A successful update publishes at most one commit. commit.changes maps the before document to the after document across every changed root. commit.inverseChanges maps it back.

Export a canonical change

const commit = editor.read.lastCommit();
 
if (commit && !commit.changes.empty) {
  sendToPeers({
    change: commit.changes.toJSON(),
    effects: commit.effects
      .filter((effect) => effect.type.collab === "shared")
      .map(encodeEditorEffect),
    tags: commit.tags,
  });
}
const commit = editor.read.lastCommit();
 
if (commit && !commit.changes.empty) {
  sendToPeers({
    change: commit.changes.toJSON(),
    effects: commit.effects
      .filter((effect) => effect.type.collab === "shared")
      .map(encodeEditorEffect),
    tags: commit.tags,
  });
}

Serialize document changes with toJSON(). Shared effects use versioned, keyed codecs declared by their descriptors. The adapter resolves the key to an installed descriptor before calling decodeEditorEffect. Runtime ids, selections, and DOM state are local unless the adapter defines an explicit awareness codec.

Import a canonical change

import { DocumentChange } from "plitejs";
import { YjsUpdatePolicy } from "plitejs/yjs";
 
editor.update(YjsUpdatePolicy.remote, (tx) => {
  tx.changes.apply(DocumentChange.fromJSON(message.change));
 
  for (const effect of decodeInstalledSharedEffects(editor, message.effects)) {
    tx.effects.emit(effect.type, effect.value);
  }
});
import { DocumentChange } from "plitejs";
import { YjsUpdatePolicy } from "plitejs/yjs";
 
editor.update(YjsUpdatePolicy.remote, (tx) => {
  tx.changes.apply(DocumentChange.fromJSON(message.change));
 
  for (const effect of decodeInstalledSharedEffects(editor, message.effects)) {
    tx.effects.emit(effect.type, effect.value);
  }
});

decodeInstalledSharedEffects represents the host adapter's registry lookup: unknown keys and codec-version mismatches stay pending until the matching descriptor is installed.

YjsUpdatePolicy.remote is also available to custom host adapters that do not install yjs(...). It marks the canonical import; it does not own a provider, readiness source, or transport lifetime.

Applying a change and its effects in one update keeps multi-root document and state transitions atomic. History, React, plugin listeners, and selectors observe the same final commit.

Subscribe to commits

const unsubscribe = editor.subscribeCommit((commit) => {
  saveDocument(editor.read.value());
 
  if (commit.tags.includes("remote-yjs-import")) return;
  if (commit.changes.empty) return;
 
  sendToPeers({
    change: commit.changes.toJSON(),
    effects: commit.effects
      .filter((effect) => effect.type.collab === "shared")
      .map(encodeEditorEffect),
    tags: commit.tags,
  });
});
const unsubscribe = editor.subscribeCommit((commit) => {
  saveDocument(editor.read.value());
 
  if (commit.tags.includes("remote-yjs-import")) return;
  if (commit.changes.empty) return;
 
  sendToPeers({
    change: commit.changes.toJSON(),
    effects: commit.effects
      .filter((effect) => effect.type.collab === "shared")
      .map(encodeEditorEffect),
    tags: commit.tags,
  });
});

Call unsubscribe() when the adapter disconnects.

Commit shape

FieldUse
changesCanonical root-aware document delta.
inverseChangesExact inverse used by history and rollback tooling.
effectsTyped state and integration effects.
annotationsTransaction metadata combined by descriptor policy.
tagsOrdered lifecycle labels.
selectionBefore / selectionAfterModel selections around the commit.
changedLazy document, root, range, and runtime-id queries derived from changes and retained snapshot indexes.

Commands and tags can identify the update that produced a commit. Adapters still serialize or lower commit.changes; there is no parallel replay stream.

Runtime ids are local

Runtime ids survive local path changes and drive React and DOM projection. They are not serialized remote identity. Use the collaboration layer's own relative positions and awareness ids for cursors and presence.

Adapter ownership

OwnerResponsibilities
PliteImmutable snapshots, canonical changes, effects, commits, anchors, and transaction boundaries.
ApplicationProvider, transport, persistence, authentication, room, readiness, and seed ownership.
Collaboration adapterAdmission, canonical change translation, effect codecs, and awareness projection.
ReactRender the committed projection and subscribe through Plite React.

plitejs/yjs is Plite's Yjs adapter. Applications own providers, authentication, room naming, readiness, seed decisions, persistence, and server policy.