ワンクリックで
yjs-document-storage
Y.js document storage guidelines for concurrent-edit safety, schema evolution, and minimal CRDT overhead
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Y.js document storage guidelines for concurrent-edit safety, schema evolution, and minimal CRDT overhead
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
| name | yjs-document-storage |
| description | Y.js document storage guidelines for concurrent-edit safety, schema evolution, and minimal CRDT overhead |
| license | MIT |
| metadata | {"version":"1.0.0"} |
Version: 1.0.0
This skill provides guidelines for encoding application data into Y.js documents. A Y.Doc is the source of truth for your data — once clients are syncing against a particular structure, changing that structure is a migration, not a refactor. Treat a Y.Doc layout like a database schema or wire protocol: correct now, extendable later, and shaped around how users actually concurrently edit.
The goal: maximize concurrent-edit safety and minimize CRDT overhead (storage, sync payload, observer churn).
Apply these guidelines when:
Y.Map.set, Y.Array.insert, new Y.Text, or serializes structures into Y.js valuesDesign for your domain's growth axes. Use maps keyed by ID for collections. Reserve top-level keys for major concepts. Prefer flat, typed values over deeply nested structures. Never serialize a whole object as a JSON string into one Y.js value — you lose granular merging and every concurrent edit conflicts with every other edit.
For each pair of fields ask: "Will two users ever edit these at the same time?"
updatedBy + updatedAt) → one atomic value.title vs. status) → separate entries.Y.Text. Selected from a set → plain value.Match Y.js granularity to your UI's editing surface.
Array<Object> → Map<id, Object> (CRITICAL)Y.Array<Object> is fine for append-only lists (logs, chat), but breaks for user-editable collections. A "move" is actually delete + insert, which creates a new item — concurrent edits to the "moved" original are lost, and two users reordering can produce duplicates. Index-based references also shift on concurrent inserts.
Use Y.Map<id, Y.Map<field, value>> so each entity has a stable identity. For user-controlled ordering, use fractional indexing (string indices between neighbors) rather than integer positions, which collide when two users reorder at the same time.
Y.Map value writes are last-writer-wins by clientID (the client with the higher clientID wins — not the later timestamp). Concurrent writes of different string values don't corrupt each other (no character-level merge happens on a Y.Map string — that only happens with Y.Text). But numbers are still preferred for known enums:
Y.Text vs. plain-value guidance in principle 9.Not everything needs granular CRDT storage:
Remove redundant fields: ID is already the map key; parent context (e.g. collectionId) is the containing map; computable values (e.g. size) come from .size.
Wrap related set calls in yDoc.transact(() => { ... }) so they apply as one update — one sync message, one observer event, consistent state.
For 2–3 field objects that always change together (e.g. { type, id }), encode as a single string ("user:123"). A nested Y.Map wastes CRDT metadata on concurrency you'll never use.
Y.Text for Collaboratively Edited Strings (HIGH)Y.Text uses a character-level CRDT that merges concurrent keystrokes. Use it for strings users type into. For strings swapped wholesale (dropdown selection), use a plain value.
On initial write, skip fields whose value matches a well-known default. Don't delete a field when a user sets it back to default — the delete is itself a CRDT op with overhead and causes churn. This is an initial-payload optimization only.
Permissions can't be enforced inside a Y.Doc — any client that can sync it reads and writes everything. Split into an index Y.Doc (IDs + metadata) and per-entity Y.Docs (content) when:
Don't split when permissions are uniform and the overhead isn't justified.
Cursor positions, text selections, typing indicators, "who's online," hover state — put these in the Awareness protocol, not the Y.Doc. Awareness is:
clientID by default, so no concurrent-write problem.Putting ephemeral state in the Y.Doc pollutes history, accumulates tombstones forever (see #16), and syncs noise to every peer on every move.
Y.Map is LWW-by-clientID, so read-modify-write loses writes under concurrency — ymap.set("count", ymap.get("count") + 1) silently drops one increment when two clients run it at once.
Pattern: give each client its own key and aggregate on read.
// Bad: concurrent increments lose writes
ymap.set("count", (ymap.get("count") ?? 0) + 1);
// Good: each client has its own key
const counts = ymap.get("counts") as Y.Map<number>;
counts.set(String(ydoc.clientID), (counts.get(String(ydoc.clientID)) ?? 0) + 1);
// Read: [...counts.values()].reduce((a, b) => a + b, 0)
Apply to view counts, reaction tallies, "who's in this room" sets, and any other accumulating shared value.
Yjs does not clone values when you set or get them. Mutating a returned object changes local state without firing any update, causing silent divergence from remote peers:
// Bad: mutates in place — no update fires, remote peers never see the change
const meta = ymap.get("meta");
meta.foo = "bar"; // silently diverges
// Good: read, derive a new value, set it back
const meta = { ...ymap.get("meta"), foo: "bar" };
ymap.set("meta", meta);
Prefer primitives or nested Y.Map for mutable structures. Reserve JSON blobs for data that's always rewritten wholesale (see #5).
A Y.Map, Y.Array, or Y.Text is bound to its parent forever. "Moving" a shared type between containers is actually copy-and-delete, which:
Design nesting so you don't need to relocate shared types. If data logically moves between containers, store a reference (ID) rather than the shared type itself, and keep the actual shared type in a flat top-level map.
Every ymap.set(key, ...) creates a new internal item and tombstones the previous one. Tombstones are not garbage-collected across sets — they accumulate on high-churn keys.
Consequences:
Mitigations: keep ephemeral high-frequency state in Awareness (#12); for genuinely needed high-churn keys, consider a separate Y.Doc that can be rotated/rebuilt periodically.
When you need the permission-split pattern from #11, subdocuments are a first-class Yjs feature: embed a Y.Doc inside a shared type in a parent doc. Subdocuments have GUID-based identity, are lazily loaded by default (autoLoad: false), and can be managed by providers as part of the parent doc's sync lifecycle.
const rootDoc = new Y.Doc();
const pageDoc = new Y.Doc(); // subdocument
rootDoc.getMap("pages").set("page-1", pageDoc);
pageDoc.load(); // lazy-load on demand
Subdocuments are cleaner than juggling two independent top-level docs when the lifecycle is "parent-known."
ydoc.getMap("foo") creates a root-level type that lives forever — you can clear() its contents, but the type itself can't be removed. This nudges a common pattern:
// Fewer root types → more control over deletion
const data = ydoc.getMap("data");
data.set("page-1", yPageMap);
data.delete("page-1"); // sub-entries CAN be deleted
Keep a single root getMap("data") (or a handful of well-known roots like data, schema, meta) and nest everything else under it, so you can prune arbitrary subtrees later.
Y.Array<Object> for concurrently edited collections — use Y.Map<id, ...>.clientID wins. Use single-writer keys (e.g. ymap.set(ydoc.clientID, value)) or an external timestamp-based LWW layer for counters/presence.Y.Array for user-reorderable lists — moves are delete+insert and lose concurrent edits. Use Y.Map<id, ...> with fractional-index ordering.index fields for ordering — two users reordering simultaneously produce duplicate indices. Use fractional indexing.id as a field when it's already the map key).clientID drops concurrent increments. Use single-writer keys.ymap.get() — Yjs doesn't clone; mutations silently diverge.rules/schema-design.md)rules/concurrency.md)Array<Object> → Map<id, Object> with fractional indexingY.Text only for collaboratively typed stringsrules/optimization.md)References: