Yjs in Next.js 16, tRPC, and shadcn/ui
Yjs 13.6.31 in an App Router codebase — the decisions collaborative editing forces and how to settle them, written so an agent applies them while writing or reviewing code. Each rule names the wrong default it corrects; there is no rule for things the model already gets right.
Most of these failures are silent. Yjs converges, the editor keeps working, and the data loss surfaces later as "my change reverted" — so the rules below lead with the evidence of the failure, most of it measured directly against Yjs 13.6.31 rather than asserted.
When to Apply
- Building collaborative editing, presence, or offline sync into a Next.js 16 App Router app
- Choosing shared types for a document model, or reviewing one that loses edits under concurrency
- Deciding where the sync backend runs, or wiring tRPC procedures alongside a Yjs provider
- Persisting
Y.Doc state to a database, or compacting stored updates
- Binding a Yjs document to React — provider lifecycle, subscriptions, Strict Mode, SSR
- Wiring Tiptap or shadcn/ui form controls to a live document
- Debugging a document that syncs but duplicates, blanks, or reverts content
This skill does not cover general Next.js App Router patterns (use the nextjs skill), general tRPC usage (trpc), or shadcn/ui component conventions (shadcn) — only where those intersect with a CRDT.
Version Pin
Rules target Yjs 13.6.31 with y-websocket 3.0.0, y-protocols 1.0.7, and y-indexeddb 9.0.12. Yjs is mid-migration to v14 under the @y/* scope, and several packages have already moved latest or main onto that prerelease track — @y/websocket-server@0.1.5 depends on yjs@^14.0.0-7. See host-pin-the-yjs-13-track before installing anything.
Rule Categories
| # |
Category |
Prefix |
Covers |
| 1 |
Sync Topology & Dependencies |
host- |
Where the sync loop runs, which backend to build on, where the trust boundary sits, which versions are on the v13 track |
| 2 |
Binary Transport & Persistence |
wire- |
Moving Uint8Array updates across JSON boundaries and into storage |
| 3 |
Modeling State in Shared Types |
model- |
Which shared type per field — decides whether concurrent edits merge or destroy each other |
| 4 |
Doc Lifecycle & React Binding |
react- |
One document across renders, Strict Mode, subscriptions, the server boundary |
| 5 |
Undo, Snapshots & Offline Load |
hist- |
Origin-scoped history, garbage collection, load ordering |
| 6 |
Awareness & Cursors |
pres- |
Ephemeral presence and positions that survive concurrent edits |
| 7 |
Editor & Form Integration |
ui- |
Tiptap and shadcn/ui form controls against a live document |
Quick Reference
1. Sync Topology & Dependencies
host-route-handlers-cannot-upgrade — App Router has no socket-upgrade API; the sync server needs a life independent of a request
host-vercel-lacks-instance-affinity — Vercel serves WebSockets but does not pin a room to an instance; in-memory registries split one document in two
host-websocket-server-is-not-production — the reference server is a starting point, and YPERSISTENCE no longer exists
host-pin-the-yjs-13-track — installing latest pulls a Yjs 14 prerelease into a v13 app
host-trpc-is-not-the-sync-channel — SSE is one-way and binary input is POST-only; use tRPC for authorization and snapshots
host-authorize-rooms-not-updates — a CRDT update cannot be schema-validated or permission-checked, so the room is the trust boundary
2. Binary Transport & Persistence
wire-rehydrate-to-uint8array — applyUpdate accepts a number[] and silently applies nothing
wire-base64-not-superjson — superjson round-trips typed arrays at 3.69x payload; base64 costs 1.33x
wire-send-updates-not-state — 24 bytes of delta versus 5041 bytes of full state for one keystroke
wire-compaction-needs-a-doc-roundtrip — mergeUpdates re-encodes but never reclaims deleted content
3. Modeling State in Shared Types
model-nested-types-not-plain-objects — a plain object in a Y.Map is one conflict unit; concurrent edits to different fields lose one
model-yarray-has-no-move — delete-then-insert duplicates the row when two people drag it at once
model-ytext-only-for-prose — two people retyping a Y.Text title produce both values spliced together
model-seed-the-document-once — an emptiness check passes on every client, so the template lands twice
4. Doc Lifecycle & React Binding
react-stable-doc-and-provider — own creation and teardown in one effect, because Strict Mode runs setup, cleanup, setup
react-subscribe-via-usesyncexternalstore — no official binding exists, and toJSON() returns a new reference every call
react-keep-yjs-client-only — providers need browser globals, and 'use client' still renders on the server
5. Undo, Snapshots & Offline Load
hist-scope-undo-by-origin — the default undo manager tracks remote edits and will revert a colleague's work
hist-snapshots-require-gc-disabled — decided at construction; restoring a snapshot otherwise throws
hist-wait-for-indexeddb-sync — an unloaded document is indistinguishable from an empty one
6. Awareness & Cursors
pres-awareness-is-ephemeral — presence in the document is permanent, versioned, and never cleaned up
pres-relative-positions-for-cursors — an index stops meaning the same place as soon as anyone types above it
7. Editor & Form Integration
ui-do-not-drive-inputs-from-the-doc — assigning value is specified to throw the caret to the end of the field
ui-tiptap-collaboration-wiring — Tiptap v3 uses its own fork, and content re-seeds on every load
How to Use
Read a reference file when its decision comes up. Each rule names the wrong default it corrects, then shows the canonical way, with an incorrect/correct contrast where the wrong way is a real trap.
When debugging rather than writing, start from the symptom:
| Symptom |
Read first |
| Document loads blank |
wire-rehydrate-to-uint8array, hist-wait-for-indexeddb-sync |
| Content appears twice |
model-seed-the-document-once, ui-tiptap-collaboration-wiring |
| Someone's edit silently reverted |
model-nested-types-not-plain-objects, hist-scope-undo-by-origin |
| List items duplicate when dragged |
model-yarray-has-no-move |
| Two editors never see each other |
host-vercel-lacks-instance-affinity |
| A user edited a field they should not reach |
host-authorize-rooms-not-updates |
| A collaborator's avatar lingers after they leave |
pres-awareness-is-ephemeral |
| Caret jumps to the end while typing |
ui-do-not-drive-inputs-from-the-doc |
| Infinite render loop or snapshot error |
react-subscribe-via-usesyncexternalstore |
| Works in production, broken in dev |
react-stable-doc-and-provider |
Reference Files
| File |
Description |
| references/_sections.md |
Category definitions and ordering |
| assets/templates/_template.md |
Template for new rules |
| AGENTS.md |
Auto-built table of contents across all rules |
| metadata.json |
Version and source references |
Related Skills
nextjs — App Router caching, Server Components, and routing outside the CRDT layer
trpc — tRPC v11 router, link, and subscription rules in full
shadcn — component composition and form patterns this skill builds on
zod — the validators used in the tRPC procedure examples here
1---2name: yjs-nextjs3description: Corrects the wrong defaults a capable model has when building Yjs collaborative editing into a Next.js 16 App Router app with tRPC v11 and shadcn/ui — pinned to Yjs 13.6.31. Covers the decisions whose failure mode is silence rather than an error — plain objects in a Y.Map discarding concurrent edits, updates that vanish because a JSON boundary turned a Uint8Array into a number array, undo that reverts a colleague's paragraph, initial content seeded once per client, and carets thrown to the end of a shadcn Input on every remote keystroke. Also covers where the sync loop can actually run, since route handlers cannot upgrade, Vercel Functions have no instance affinity, and tRPC subscriptions are one-way — plus which packages have already moved to the Yjs 14 prerelease track. Use when writing, reviewing, or debugging collaborative editing, presence, offline sync, or CRDT persistence in this stack.4---5
6# Yjs in Next.js 16, tRPC, and shadcn/ui
7
8Yjs 13.6.31 in an App Router codebase — the decisions collaborative editing forces and how to settle them, written so an agent applies them while writing or reviewing code. Each rule names the wrong default it corrects; there is no rule for things the model already gets right.
9
10Most of these failures are silent. Yjs converges, the editor keeps working, and the data loss surfaces later as "my change reverted" — so the rules below lead with the evidence of the failure, most of it measured directly against Yjs 13.6.31 rather than asserted.
11
12## When to Apply
13
14- Building collaborative editing, presence, or offline sync into a Next.js 16 App Router app
15- Choosing shared types for a document model, or reviewing one that loses edits under concurrency
16- Deciding where the sync backend runs, or wiring tRPC procedures alongside a Yjs provider
17- Persisting `Y.Doc` state to a database, or compacting stored updates
18- Binding a Yjs document to React — provider lifecycle, subscriptions, Strict Mode, SSR
19- Wiring Tiptap or shadcn/ui form controls to a live document
20- Debugging a document that syncs but duplicates, blanks, or reverts content
21
22This skill does **not** cover general Next.js App Router patterns (use the `nextjs` skill), general tRPC usage (`trpc`), or shadcn/ui component conventions (`shadcn`) — only where those intersect with a CRDT.
23
24## Version Pin
25
26Rules target **Yjs 13.6.31** with `y-websocket` 3.0.0, `y-protocols` 1.0.7, and `y-indexeddb` 9.0.12. Yjs is mid-migration to v14 under the `@y/*` scope, and several packages have already moved `latest` or `main` onto that prerelease track — `@y/websocket-server@0.1.5` depends on `yjs@^14.0.0-7`. See [`host-pin-the-yjs-13-track`](references/host-pin-the-yjs-13-track.md) before installing anything.
27
28## Rule Categories
29
30| # | Category | Prefix | Covers |
31|---|----------|--------|--------|
32| 1 | Sync Topology & Dependencies | `host-` | Where the sync loop runs, which backend to build on, where the trust boundary sits, which versions are on the v13 track |
33| 2 | Binary Transport & Persistence | `wire-` | Moving `Uint8Array` updates across JSON boundaries and into storage |
34| 3 | Modeling State in Shared Types | `model-` | Which shared type per field — decides whether concurrent edits merge or destroy each other |
35| 4 | Doc Lifecycle & React Binding | `react-` | One document across renders, Strict Mode, subscriptions, the server boundary |
36| 5 | Undo, Snapshots & Offline Load | `hist-` | Origin-scoped history, garbage collection, load ordering |
37| 6 | Awareness & Cursors | `pres-` | Ephemeral presence and positions that survive concurrent edits |
38| 7 | Editor & Form Integration | `ui-` | Tiptap and shadcn/ui form controls against a live document |
39
40## Quick Reference
41
42### 1. Sync Topology & Dependencies
43
44- [`host-route-handlers-cannot-upgrade`](references/host-route-handlers-cannot-upgrade.md) — App Router has no socket-upgrade API; the sync server needs a life independent of a request
45- [`host-vercel-lacks-instance-affinity`](references/host-vercel-lacks-instance-affinity.md) — Vercel serves WebSockets but does not pin a room to an instance; in-memory registries split one document in two
46- [`host-websocket-server-is-not-production`](references/host-websocket-server-is-not-production.md) — the reference server is a starting point, and `YPERSISTENCE` no longer exists
47- [`host-pin-the-yjs-13-track`](references/host-pin-the-yjs-13-track.md) — installing `latest` pulls a Yjs 14 prerelease into a v13 app
48- [`host-trpc-is-not-the-sync-channel`](references/host-trpc-is-not-the-sync-channel.md) — SSE is one-way and binary input is POST-only; use tRPC for authorization and snapshots
49- [`host-authorize-rooms-not-updates`](references/host-authorize-rooms-not-updates.md) — a CRDT update cannot be schema-validated or permission-checked, so the room is the trust boundary
50
51### 2. Binary Transport & Persistence
52
53- [`wire-rehydrate-to-uint8array`](references/wire-rehydrate-to-uint8array.md) — `applyUpdate` accepts a `number[]` and silently applies nothing
54- [`wire-base64-not-superjson`](references/wire-base64-not-superjson.md) — superjson round-trips typed arrays at 3.69x payload; base64 costs 1.33x
55- [`wire-send-updates-not-state`](references/wire-send-updates-not-state.md) — 24 bytes of delta versus 5041 bytes of full state for one keystroke
56- [`wire-compaction-needs-a-doc-roundtrip`](references/wire-compaction-needs-a-doc-roundtrip.md) — `mergeUpdates` re-encodes but never reclaims deleted content
57
58### 3. Modeling State in Shared Types
59
60- [`model-nested-types-not-plain-objects`](references/model-nested-types-not-plain-objects.md) — a plain object in a `Y.Map` is one conflict unit; concurrent edits to different fields lose one
61- [`model-yarray-has-no-move`](references/model-yarray-has-no-move.md) — delete-then-insert duplicates the row when two people drag it at once
62- [`model-ytext-only-for-prose`](references/model-ytext-only-for-prose.md) — two people retyping a `Y.Text` title produce both values spliced together
63- [`model-seed-the-document-once`](references/model-seed-the-document-once.md) — an emptiness check passes on every client, so the template lands twice
64
65### 4. Doc Lifecycle & React Binding
66
67- [`react-stable-doc-and-provider`](references/react-stable-doc-and-provider.md) — own creation and teardown in one effect, because Strict Mode runs setup, cleanup, setup
68- [`react-subscribe-via-usesyncexternalstore`](references/react-subscribe-via-usesyncexternalstore.md) — no official binding exists, and `toJSON()` returns a new reference every call
69- [`react-keep-yjs-client-only`](references/react-keep-yjs-client-only.md) — providers need browser globals, and `'use client'` still renders on the server
70
71### 5. Undo, Snapshots & Offline Load
72
73- [`hist-scope-undo-by-origin`](references/hist-scope-undo-by-origin.md) — the default undo manager tracks remote edits and will revert a colleague's work
74- [`hist-snapshots-require-gc-disabled`](references/hist-snapshots-require-gc-disabled.md) — decided at construction; restoring a snapshot otherwise throws
75- [`hist-wait-for-indexeddb-sync`](references/hist-wait-for-indexeddb-sync.md) — an unloaded document is indistinguishable from an empty one
76
77### 6. Awareness & Cursors
78
79- [`pres-awareness-is-ephemeral`](references/pres-awareness-is-ephemeral.md) — presence in the document is permanent, versioned, and never cleaned up
80- [`pres-relative-positions-for-cursors`](references/pres-relative-positions-for-cursors.md) — an index stops meaning the same place as soon as anyone types above it
81
82### 7. Editor & Form Integration
83
84- [`ui-do-not-drive-inputs-from-the-doc`](references/ui-do-not-drive-inputs-from-the-doc.md) — assigning `value` is specified to throw the caret to the end of the field
85- [`ui-tiptap-collaboration-wiring`](references/ui-tiptap-collaboration-wiring.md) — Tiptap v3 uses its own fork, and `content` re-seeds on every load
86
87## How to Use
88
89Read a reference file when its decision comes up. Each rule names the wrong default it corrects, then shows the canonical way, with an incorrect/correct contrast where the wrong way is a real trap.
90
91When debugging rather than writing, start from the symptom:
92
93| Symptom | Read first |
94|---------|-----------|
95| Document loads blank | [`wire-rehydrate-to-uint8array`](references/wire-rehydrate-to-uint8array.md), [`hist-wait-for-indexeddb-sync`](references/hist-wait-for-indexeddb-sync.md) |
96| Content appears twice | [`model-seed-the-document-once`](references/model-seed-the-document-once.md), [`ui-tiptap-collaboration-wiring`](references/ui-tiptap-collaboration-wiring.md) |
97| Someone's edit silently reverted | [`model-nested-types-not-plain-objects`](references/model-nested-types-not-plain-objects.md), [`hist-scope-undo-by-origin`](references/hist-scope-undo-by-origin.md) |
98| List items duplicate when dragged | [`model-yarray-has-no-move`](references/model-yarray-has-no-move.md) |
99| Two editors never see each other | [`host-vercel-lacks-instance-affinity`](references/host-vercel-lacks-instance-affinity.md) |
100| A user edited a field they should not reach | [`host-authorize-rooms-not-updates`](references/host-authorize-rooms-not-updates.md) |
101| A collaborator's avatar lingers after they leave | [`pres-awareness-is-ephemeral`](references/pres-awareness-is-ephemeral.md) |
102| Caret jumps to the end while typing | [`ui-do-not-drive-inputs-from-the-doc`](references/ui-do-not-drive-inputs-from-the-doc.md) |
103| Infinite render loop or snapshot error | [`react-subscribe-via-usesyncexternalstore`](references/react-subscribe-via-usesyncexternalstore.md) |
104| Works in production, broken in dev | [`react-stable-doc-and-provider`](references/react-stable-doc-and-provider.md) |
105
106## Reference Files
107
108| File | Description |
109|------|-------------|
110| [references/_sections.md](references/_sections.md) | Category definitions and ordering |
111| [assets/templates/_template.md](assets/templates/_template.md) | Template for new rules |
112| [AGENTS.md](AGENTS.md) | Auto-built table of contents across all rules |
113| [metadata.json](metadata.json) | Version and source references |
114
115## Related Skills
116
117- [`nextjs`](../../.curated/nextjs/SKILL.md) — App Router caching, Server Components, and routing outside the CRDT layer
118- [`trpc`](../trpc/SKILL.md) — tRPC v11 router, link, and subscription rules in full
119- [`shadcn`](../../.curated/shadcn/SKILL.md) — component composition and form patterns this skill builds on
120- [`zod`](../../.curated/zod/SKILL.md) — the validators used in the tRPC procedure examples here