---
title: "Real-Time Collaboration"
description: "Multi-user collaborative editing where the AI agent is a first-class peer: CRDT merging, live presence, SSE fast-path, and granular server-side merge — on any SQL database and any host."
---

# Real-Time Collaboration

Imagine opening a document and seeing a peer's cursor scroll to a paragraph,
then the text rewrite itself — surgically, without losing your place. That
peer might be a teammate. It might be the agent. From the framework's
perspective they are identical: both produce Yjs operations that merge
conflict-free into the shared document. This is the keystone of the
agent-native collaboration model.

## Vision {#vision}

Editing alongside the agent feels like working in Google Docs or Figma with
a coworker who is both instant and tireless:

If you just need the UI to refresh when the agent or another user writes to SQL, you don't need any of this — use [`useDbSync`](/docs/client). This page is for character-level co-editing of a single rich-text document (shared cursors, conflict-free merging). Both ride the same `/_agent-native/poll` channel.

This is built on three battle-tested technologies: **Yjs** (CRDT for conflict-free merging), **TipTap** (rich text editor), and **polling-based sync** (works in all deployment environments including serverless and edge).

- **CRDT merging** — Concurrent edits from humans and agents merge without
  conflicts. You type in one paragraph; the agent rewrites another; both
  land cleanly.
- **Presence** — A `PresenceBar` shows who is in the document right now,
  including an agent presence indicator when the agent is actively editing.
- **The agent as a peer editor** — Agent edits flow through the same Yjs
  infrastructure as human edits. They appear live, without disrupting cursor
  positions, selections, or the undo stack.
- **Works everywhere** — Any SQL database Drizzle supports (SQLite, Postgres).
  Any hosting target Nitro supports, including serverless and edge.

## Architecture {#architecture}

The collaboration system has five interlocking layers.

### 1. Yjs Y.Doc (CRDT layer)

Each collaborative document is a `Y.Doc` containing shared types — usually a
`Y.XmlFragment` for rich text (the ProseMirror node tree that TipTap reads) or
`Y.Map` / `Y.Array` for structured JSON data. Yjs merges concurrent updates
with no central coordinator; any two clients that exchange their state reach
the same result regardless of order.

### 2. SQL canonical content (durable source of truth)

Yjs state is persisted in a `_collab_docs` table as base64-encoded binary.
The table is framework-managed and provider-agnostic (SQLite and Postgres use
identical schemas). Each row carries an optimistic-concurrency version column
to prevent concurrent write races. Tombstone compaction runs opportunistically
when the stored blob exceeds 4× the freshly encoded state — no background job
required.

### 3. `updatedAt`-gated reconcile (agent-edit propagation)

Agent actions do not push into Yjs in-process. Instead, the action edits the
canonical SQL content column and bumps `updatedAt`. The change-sync system
detects the bump, the open editor refetches the record, and the lead client
applies the new content into the shared Y.Doc via `setContent`. An `updatedAt`
gate ensures only genuinely newer content is adopted — lagging poll responses
cannot revert the edit.

### 4. Lead-client election (deduplication)

When multiple tabs are open, exactly one applies an authoritative SQL snapshot
into the shared Y.Doc. The lead is the tab with the lowest Yjs `clientID`
among currently visible peers. The agent's awareness entry uses
`AGENT_CLIENT_ID` (max int) so it can never be the lead. A client editing
alone is always the lead. The election is deterministic with no coordination
round-trip (`isReconcileLeadClient` from `@agent-native/core/client`).

### 5. SSE fast-path + polling fallback (transport)

Collab update events travel via two paths:

- **SSE fast-path** — The client subscribes to `/_agent-native/poll-events`
  (the same `EventSource` used by `useDbSync`). Collab update events arrive
  push-style, typically in tens of milliseconds. While SSE is healthy the
  poll loop relaxes to a slow cadence (~12 s by default).
- **Polling fallback** — `/_agent-native/poll?since=N` is polled every 2 s
  when SSE is unavailable. This makes collaboration work on any deployment
  target — including serverless functions where persistent connections are
  impossible and different invocations can handle different requests.

Local Yjs updates are debounced and coalesced with `Y.mergeUpdates` (~80 ms)
before being sent to the server, reducing keystroke-level network traffic.
The batch is flushed immediately on `visibilitychange` or `pagehide`. A
state-vector diff (`GET /:docId/state?stateVector=…`) is fetched only on
reconnect, ring-buffer overflow, or every 15th poll cycle — not on every
cycle.

Network errors use exponential backoff with jitter, capped at ~15 s.

```
Browser                Server             SQL
──────                 ──────             ───
[Human edits]
  → Y.Doc update
  → debounce ~80ms
  → POST /collab/:docId/update ──────→ apply + persist
                                       emitCollabUpdate
                       ← SSE push ──── poll-events stream
  ← Y.applyUpdate

[Agent action]
  action writes SQL content + bumps updatedAt
  ← change-sync detects updatedAt bump
  ← lead client calls setContent
  → Y.Doc update
  → POST /collab/:docId/update ──────→ apply + persist
  ← SSE push to all other clients
```

## Quickstart {#quickstart}

### 1. Install packages

```bash
pnpm add @tiptap/extension-collaboration @tiptap/extension-collaboration-caret @tiptap/y-tiptap @tiptap/core
```

### 2. Add Vite optimizeDeps

Prevents Vite from re-bundling TipTap in incompatible ways during dev:

```typescript
// vite.config.ts
export default defineConfig({
  plugins: [reactRouter()],
  optimizeDeps: {
    include: [
      "yjs",
      "y-protocols/awareness",
      "@tiptap/core",
      "@tiptap/extension-collaboration",
      "@tiptap/extension-collaboration-caret",
      "@tiptap/y-tiptap",
    ],
  },
});
```

### 3. Add the collab server plugin

Always set `resourceType` to the name of the shareable resource registered
via `registerShareableResource`. Without it, collab push events are delivered
to all authenticated users without document-level scoping, and the server
logs a one-time warning.

```typescript
// server/plugins/collab.ts
import { createCollabPlugin } from "@agent-native/core/server";

export default createCollabPlugin({
  table: "documents",
  contentColumn: "content",
  idColumn: "id",
  resourceType: "document", // required for access-scoped event delivery
});
```

### 4. Use the client hook

```typescript
import {
  useCollaborativeDoc,
  emailToColor,
  emailToName,
} from "@agent-native/core/client";

const TAB_ID = generateTabId(); // or Math.random().toString(36)

const { ydoc, awareness, isLoading, activeUsers, agentActive, agentPresent } =
  useCollaborativeDoc({
    docId: documentId,
    requestSource: TAB_ID,
    user: {
      name: emailToName(session.email),
      email: session.email,
      color: emailToColor(session.email),
    },
  });
```

### 5. Add TipTap extensions

```typescript
import Collaboration from "@tiptap/extension-collaboration";
import CollaborationCaret from "@tiptap/extension-collaboration-caret";

const editor = useEditor({
  extensions: [
    StarterKit.configure({ history: false }), // Yjs owns undo
    Collaboration.configure({ document: ydoc }),
    CollaborationCaret.configure({
      provider: { awareness },
      user: { name, color },
    }),
  ],
  // Do NOT pass content here — Yjs owns the content
});
```

### 6. Seed on first load (if content exists)

The Collaboration extension does not auto-seed from a `content` prop. If the
Y.Doc is empty and the document has existing content, seed it:

```typescript
useEffect(() => {
  if (!ydoc || !editor || !isLoaded) return;
  const fragment = ydoc.getXmlFragment("default");
  if (fragment.length === 0 && initialContent) {
    editor.commands.setContent(initialContent);
  }
}, [ydoc, editor, isLoaded]);
```

User identity is derived from the session email. The framework provides `emailToColor()` and `emailToName()` helpers to generate consistent cursor colors and display names from email addresses.

## Comments {#comments}

Templates can add a comments system with threaded discussions on documents. The content template's comments system includes a full implementation with:

- `document_comments` SQL table (threads, replies, resolved status)
- The content template's REST routes for update/delete at `/api/comments/:id`; create and list run through the `add-comment` / `list-comments` actions. Custom templates implement their own equivalent endpoints against the core `POST /_agent-native/collab/:docId/search-replace` route.
- Comments sidebar with threaded view and reply UI
- Resolve/unresolve threads
- **Send to AI** button — sends the comment thread context to the agent chat via `sendToAgentChat()`
- Agent actions: `list-comments`, `add-comment`
- Notion comment sync: `sync-notion-comments` action for bidirectional pull/push

## Collab routes {#collab-routes}

All collab routes are auto-mounted under `/_agent-native/collab/` by the collab plugin:

| Route                         | Purpose                                  |
| ----------------------------- | ---------------------------------------- |
| `GET /:docId/state`           | Fetch full Y.Doc state (base64)          |
| `POST /:docId/update`         | Apply client Yjs update                  |
| `POST /:docId/text`           | Apply full text replacement (diff-based) |
| `POST /:docId/search-replace` | Surgical find/replace in Y.XmlFragment   |
| `POST /:docId/awareness`      | Sync cursor/presence state               |
| `GET /:docId/users`           | List active users on a document          |

## Agent edit action {#edit-document}

The content template's `edit-document` action is the primary way agents make changes to documents in collaborative mode:

```bash
# Single edit
pnpm action edit-document --id doc123 --find "old text" --replace "new text"

# Batch edits
pnpm action edit-document --id doc123 --edits '[{"find":"old","replace":"new"}]'

# Delete text
pnpm action edit-document --id doc123 --find "delete me" --replace ""
```

---

## Presence Kit {#presence-kit}

The presence kit provides Liveblocks/Figma-grade live-cursor and selection primitives on top of the existing awareness layer.

Import client-side presence and editor UI from the focused browser subpath:

```ts
import {
  PresenceBar,
  LiveCursorOverlay,
  RemoteSelectionRings,
  useCollaborativeDoc,
  usePresence,
} from "@agent-native/core/client/collab";
```

Server-side agent presence helpers stay in the lower-level collab package:

```ts
import {
  agentEnterDocument,
  agentLeaveDocument,
  agentUpdateSelection,
} from "@agent-native/core/collab";
```

### Public API {#presence-public-api}

| API                                                 | Purpose                                                                                                                                                |
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `useCollaborativeDoc(options)`                      | Creates the stable `Y.Doc` and awareness instance, handles state-vector sync, SSE fast-path, polling fallback, active users, and agent presence flags. |
| `usePresence(awareness, localClientId)`             | Derives remote participants and publishes arbitrary local awareness fields such as cursor, selection, viewport, or tool mode.                          |
| `<PresenceBar>`                                     | Renders active collaborators plus the AI agent, with optional avatar-click follow mode wiring.                                                         |
| `<LiveCursorOverlay>`                               | Renders remote cursor labels over a positioned container from normalized 0-1 coordinates.                                                              |
| `<RemoteSelectionRings>`                            | Renders colored rings and labels around selected DOM elements resolved by your app.                                                                    |
| `useFollowUser(options)`                            | Invokes a callback when the followed participant publishes viewport changes.                                                                           |
| `toNormalized()` / `fromNormalized()`               | Convert pointer coordinates to/from normalized container coordinates.                                                                                  |
| `dedupeCollabUsersByEmail()`                        | Build custom avatar stacks without one user showing once per open tab.                                                                                 |
| `useCollaborativeMap()` / `useCollaborativeArray()` | Client hooks for Y.Map/Y.Array structured collaboration. Treat as lower-level until a template proves the exact product pattern.                       |

`UseCollaborativeDocOptions`:

| Option                | Description                                                         |
| --------------------- | ------------------------------------------------------------------- |
| `docId`               | Document id, or `null` to disable the hook.                         |
| `pollInterval`        | Poll interval when SSE is unavailable. Default: `2000`.             |
| `pollIntervalWithSse` | Slow poll interval while SSE is healthy. Default: `12000`.          |
| `pauseWhenHidden`     | Pause remote update/presence polling while hidden. Default: `true`. |
| `baseUrl`             | Collab endpoint prefix. Default: `/_agent-native/collab`.           |
| `requestSource`       | Stable tab/source id used to ignore self-originated refresh noise.  |
| `user`                | `{ name, email, color }` shown in cursor and presence UI.           |

`UseCollaborativeDocResult`:

| Field          | Description                                                          |
| -------------- | -------------------------------------------------------------------- |
| `ydoc`         | Stable `Y.Doc` for the current `docId`.                              |
| `awareness`    | Yjs Awareness instance used by cursors, selections, and follow mode. |
| `isLoading`    | Initial server state is still loading.                               |
| `isSynced`     | The hook has caught up to server state.                              |
| `activeUsers`  | Human collaborators from awareness.                                  |
| `agentActive`  | The agent is actively editing right now.                             |
| `agentPresent` | The agent has an awareness entry for this document.                  |

### Fast awareness {#fast-awareness}

Awareness state changes now propagate at ~150ms instead of the 2s poll cycle:

- **Client → server**: any call to `setPresence()` or `awareness.setLocalStateField()` triggers a throttled POST to `/_agent-native/collab/:docId/awareness` within 150ms, coalescing rapid changes into one request.
- **Server → clients**: the `postAwareness` handler emits an `AWARENESS_CHANGE_EVENT` after storing. The `/_agent-native/poll-events` SSE stream forwards these events push-style to connected peers. Polling-only deployments continue to work — cursors degrade to poll cadence without errors.

### `usePresence(awareness, localClientId)` {#use-presence}

Returns a reactive list of remote participants and a setter for the local presence payload:

```typescript
import { usePresence } from "@agent-native/core/client";

const { others, setPresence } = usePresence(awareness, ydoc?.clientID);

// Publish cursor position (normalized 0–1)
setPresence({ cursor: { x: 0.4, y: 0.7 }, selection: "#hero" });

// others: OtherPresence[]
// {
//   clientId: number
//   user: { name, email, color }
//   presence: { cursor?, selection?, viewport?, ... }
//   isAgent: boolean   ← true for AGENT_CLIENT_ID
// }
```

The agent (AGENT_CLIENT_ID) appears as a first-class participant with `isAgent: true`. When `agentUpdateSelection()` is called server-side, its selection metadata flows through `usePresence` like any other participant.

### `LiveCursorOverlay` {#live-cursor-overlay}

Renders remote cursors as absolutely-positioned labels over a container element:

```tsx
import { LiveCursorOverlay } from "@agent-native/core/client";

// cursor positions stored as { x, y } normalized 0–1 under presence.cursor
<div ref={containerRef} style={{ position: "relative" }}>
  {content}
  <LiveCursorOverlay
    others={others} // from usePresence
    containerRef={containerRef}
    cursorKey="cursor" // key in presence payload (default: "cursor")
  />
</div>;
```

The agent's cursor renders distinctly with a sparkle icon. Cursors fade out after 10s of inactivity with smooth CSS transitions at 120ms.

### `RemoteSelectionRings` {#remote-selection-rings}

Renders colored outline rings + name tags over remotely-selected elements:

```tsx
import { RemoteSelectionRings } from "@agent-native/core/client";

<div ref={containerRef} style={{ position: "relative" }}>
  {content}
  <RemoteSelectionRings
    others={others}
    selectionKey="selection" // key in presence payload (default: "selection")
    resolveRect={(descriptor) =>
      document.querySelector(descriptor)?.getBoundingClientRect() ?? null
    }
    containerRef={containerRef}
  />
</div>;
```

### `useFollowUser` {#follow-user}

Invoke a callback whenever the followed participant's viewport changes:

```typescript
import { useFollowUser } from "@agent-native/core/client";

const { isFollowing, stopFollowing } = useFollowUser({
  others,
  followingId, // null to stop following
  viewportKey: "viewport",
  onViewport: (vp) => {
    if (vp.fileId) setActiveFileId(vp.fileId);
    if (vp.zoom) setZoom(vp.zoom);
  },
});
```

Participants publish their viewport with `setPresence({ viewport: { fileId, zoom } })`.

### `PresenceBar` follow-mode props {#presence-bar-follow}

The `PresenceBar` component now accepts optional follow-mode props:

```tsx
<PresenceBar
  activeUsers={activeUsers}
  agentActive={agentActive}
  onAvatarClick={(user) => {
    // user is null for the agent avatar
    const email = user?.email ?? "agent@system";
    setFollowing((prev) => (prev === email ? null : email));
  }}
  followingEmail={followingEmail} // highlighted avatar + "Following X" chip
/>
```

### Normalized coordinate helpers {#norm-coords}

```typescript
import { toNormalized, fromNormalized } from "@agent-native/core/client";

// In a pointer event handler:
const norm = toNormalized(
  e.clientX,
  e.clientY,
  container.getBoundingClientRect(),
);
setPresence({ cursor: norm });

// In a cursor renderer:
const px = fromNormalized(norm, container.getBoundingClientRect());
```

### Agent cursor plumbing {#agent-cursor}

Server-side actions call `agentUpdateSelection()` to publish where the agent is working. The design template's `edit-design` and `generate-design` actions call this automatically. Other templates can do the same:

```typescript
import {
  agentEnterDocument,
  agentLeaveDocument,
  agentUpdateSelection,
} from "@agent-native/core/collab";

agentEnterDocument(docId);
agentUpdateSelection(docId, {
  selection: "#target-element",
  editingFile: "index.html",
});
try {
  // ... perform edits ...
} finally {
  agentLeaveDocument(docId);
}
```

The selection metadata flows through `usePresence` on connected clients as `other.presence.selection`.

---

## Route table {#routes}

All routes are auto-mounted under `/_agent-native/collab/` by the collab
plugin:

| Route                         | Purpose                                                     |
| ----------------------------- | ----------------------------------------------------------- |
| `GET /:docId/state`           | Full Y.Doc state (base64). Accepts `?stateVector=` for diff |
| `POST /:docId/update`         | Apply client Yjs update (base64). Max 2 MB by default       |
| `POST /:docId/text`           | Apply full text replacement (diff-based)                    |
| `POST /:docId/search-replace` | Surgical find/replace in Y.XmlFragment                      |
| `POST /:docId/json`           | Apply full JSON diff to Y.Map/Y.Array                       |
| `GET /:docId/json`            | Read current JSON state                                     |
| `POST /:docId/patch`          | Apply surgical JSON patch ops (upsert/remove/reorder)       |
| `POST /:docId/awareness`      | Sync cursor/presence state                                  |
| `GET /:docId/users`           | List active users on a document                             |

## Transport and performance {#transport}

| Property                     | Value                                                      |
| ---------------------------- | ---------------------------------------------------------- |
| Update debounce              | ~80 ms (coalesces rapid keystrokes via `Y.mergeUpdates`)   |
| Poll interval (no SSE)       | 2 s (configurable via `pollInterval`)                      |
| Poll interval (SSE healthy)  | ~12 s (configurable via `pollIntervalWithSse`)             |
| State-vector fetch frequency | On reconnect, ring-buffer gap, or every 15th poll cycle    |
| Backoff on error             | Exponential with jitter, cap ~15 s                         |
| Max payload (writes)         | 2 MB default, configurable via `maxPayloadBytes`           |
| Compaction threshold         | Stored blob > 4× fresh encoding triggers tombstone compact |
| Per-write DB reads           | 1 (CAS version read inside `persistMergedState` only)      |

## Security {#security}

### Always set `resourceType`

```typescript
createCollabPlugin({
  resourceType: "document", // the name passed to registerShareableResource
});
```

Without `resourceType` the plugin logs a warning and broadcasts collab push
events to all authenticated users on the deployment without document-level
scoping. Non-owners fall back to state-vector catch-up (safe but higher
latency) regardless of whether `resourceType` is set.

### Access checks

All collab routes require authentication. When `resourceType` is set, reads
require at least viewer access and writes require editor access, using the
same `resolveAccess` / `assertAccess` helpers as the sharing system. A 404
(not 403) is returned on access failures to avoid leaking document existence.

### Payload limits

Write routes (`update`, `text`, `json`, `patch`, `search-replace`) reject
payloads exceeding the configured limit with HTTP 413. The default is 2 MB.
Override per-plugin:

```typescript
createCollabPlugin({
  resourceType: "document",
  maxPayloadBytes: 512 * 1024, // 512 KB
});
```

### Awareness scoping

Awareness routes (`POST /awareness`, `GET /users`) are gated by the same
access check as reads — a user who lacks viewer access cannot learn who else
is editing a document.

## Patterns {#patterns}

### Granular server-side merge for structured data

For structured documents (slide decks, form builders, design files) the Yjs
body collab model can conflict when two agents or users rewrite the same
top-level record simultaneously. The safer pattern is **granular server-side
merge**: define an action that accepts a set of targeted operations and
applies them atomically, so concurrent edits to different items both survive.

**Slides (`patch-deck`)** — Instead of replacing the entire deck JSON on every
change, the action accepts per-slide operations:

```typescript
// Conceptual patch-deck action shape
type PatchDeckOp =
  | { type: "patch"; slideId: string; fields: Partial<SlideFields> }
  | { type: "add"; position: number; slide: SlideData }
  | { type: "delete"; slideId: string }
  | { type: "reorder"; slideId: string; newIndex: number };
```

Two users editing different slides both succeed; there is no LWW clobber at
the deck level.

**Forms (`patch-form-fields`)** — Field-level merge with upsert/remove/reorder
ops so concurrent edits to different form fields both survive.

Use this pattern when:

- The document is structured (items inside a container).
- Concurrent edits target different items.
- Body collab (Yjs `Y.XmlFragment`) is overkill or inapplicable.

Use body collab (Y.XmlFragment + TipTap) when:

- The document is free-form rich text where any region can be edited.
- Cursor-level CRDT merge matters.

### Collaborative undo scoping (Y.UndoManager)

The Design template uses `Y.UndoManager` to scope undo/redo to the local
user's own edits. Remote peer edits and agent edits are never undone by a
user's Cmd+Z.

```typescript
import * as Y from "yjs";

const LOCAL_EDIT_ORIGIN = "local";

const undoManager = new Y.UndoManager(ydoc.getText("content"), {
  trackedOrigins: new Set([LOCAL_EDIT_ORIGIN]),
  captureTimeout: 800, // coalesce rapid slider drags into one undo step
});

// Wrap local edits with the tracked origin
ydoc.transact(() => {
  // apply local style change
}, LOCAL_EDIT_ORIGIN);

// Undo/redo — only reverses LOCAL_EDIT_ORIGIN transactions
undoManager.undo(); // Cmd+Z
undoManager.redo(); // Shift+Cmd+Z
```

Key properties:

- `trackedOrigins` must be a `Set`. Only transactions with a matching origin
  are captured in the undo stack.
- Remote updates (origin `"remote"`) and agent updates (origin `"agent"`) are
  never captured.
- Recreate and dispose the manager when the active document changes; stale
  managers hold references that can grow unboundedly.

## Known limitations {#limitations}

- **Same-region simultaneous rewrite is LWW** — If the agent rewrites a
  passage and a human has unsaved edits in the exact same region, the
  lead-client snapshot can overwrite the human's in-flight changes. Edits in
  different regions merge correctly via the CRDT. Granular server-side merge
  (see above) avoids this for structured documents.
- **In-process write locks on serverless** — The `_writeLocks` map is
  process-local. Concurrent requests landing on different serverless
  invocations serialize at the SQL CAS layer (optimistic concurrency) rather
  than the in-memory lock. This is safe but means high-throughput scenarios on
  serverless may see more CAS retries.
- **Awareness is per-process** — The awareness in-memory store is
  process-local. Serverless / multi-process deployments see partial awareness
  state per invocation. Clients still receive full awareness snapshots on each
  poll cycle, so presence indicators update within one poll interval.

## Presence {#presence}

The `useCollaborativeDoc` hook returns:

- `activeUsers` — array of `CollabUser` (name, email, color) for all peers
  currently in the document (sourced from awareness).
- `agentActive` — `true` briefly after the agent makes an edit (use for a
  transient visual indicator).
- `agentPresent` — `true` while the agent has an active awareness entry
  (durable presence heartbeat).

Use `emailToColor(email)` and `emailToName(email)` from
`@agent-native/core/client` to generate consistent cursor colors and display
names from email addresses.

A `PresenceBar` rendered with `activeUsers` shows live human and agent
collaborators. Per-slide presence (which users are viewing a given slide)
layers on top of the same awareness state.

## Related docs {#related}

- [Real-Time Sync](/docs/real-time-sync) — the `useDbSync` + `useChangeVersion`
  system that delivers the `updatedAt` bump driving editor reconciliation.
- [Security](/docs/security) — `registerShareableResource`, `resolveAccess`,
  and `assertAccess` for the access model referenced by `resourceType`.
- [Sharing](/docs/sharing) — how documents are shared and how access is granted.
- [Template: Content](/docs/template-content) — reference implementation of
  collaborative rich-text editing.
- [Template: Slides](/docs/template-slides) — granular `patch-deck` action for
  structured concurrent editing.
- [Template: Forms](/docs/template-forms) — field-level `patch-form-fields`
  server-side merge.
- [Template: Design](/docs/template-design) — `Y.UndoManager` undo/redo scoped
  to local user edits.
