Assists with Yjs patterns, shared types, and conflict resolution for building collaborative applications.
Install
mkdir -p .claude/skills/yjs && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/2298" && unzip -o skill.zip -d .claude/skills/yjs && rm skill.zipInstalls to .claude/skills/yjs
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
Yjs CRDT patterns, shared types (Y.Map, Y.Array, Y.Text), transactions, y-protocols sync and awareness, y-indexeddb persistence, conflict resolution, and document storage. Use when mentioning Yjs, Y.Doc, CRDTs, collaborative editing, real-time sync, awareness, IndexeddbPersistence, or Yjs providers.Key capabilities
- →Configures shared data models using Y.Map and Y.Array
- →Implements fractional indexing for reordering logic
- →Encapsulates persistence strategies via indexedDB
- →Enforces state-vector sync for data consistency
How it works
It applies CRDT principles to structure collaborative state and manages transaction propagation to ensure document consistency across distributed clients.
Inputs & outputs
When to use yjs
- →Setting up shared data structures
- →Handling conflict resolution
- →Implementing real-time synchronization
- →Optimizing Yjs document storage
About this skill
Yjs 14 CRDT Patterns
Reference Repositories
- Yjs: CRDT framework for shared editing and offline-first data
- Yjs Protocols: algorithmic grounding for sync and awareness
Upstream Grounding
When conflict semantics, transaction origins, shared-type behavior, update encoding, storage growth, or shared-type APIs affect correctness, use source-backed grounding before relying on memory. If DeepWiki MCP is available, ask a narrow question against yjs/yjs; for sync and awareness algorithms, ask against yjs/y-protocols. If DeepWiki is unavailable or the repo is not indexed, use upstream source or official docs directly. Treat DeepWiki as orientation, then verify decisive details against the locally pinned @y/y types and source before changing code.
Epicenter targets @y/y 14 only. Do not add yjs 13, y-indexeddb, a compatibility reader, a package alias, a dual wire, or a fallback. Existing Yjs 13 code is replacement material, not a compatibility surface.
Skip DeepWiki for stable basics and repo-local patterns already documented below.
Read references/document-design.md before choosing how a new row document is structured. Counters, user-controlled ordering, and nested shapes each have a conflict behavior that is expensive to change once data exists.
Read references/debugging.md when a document converges to unexpected state or grows faster than its content.
Related Skills: See
sveltefor reading store data into a component, andarktypefor the expression strings a workspace is written in.
Transactions, Origins, And Undo
- Yjs updates are commutative and idempotent. A state vector describes what a
replica HAS; it does not order what it owes. A delete moves no client clock,
so two replicas can hold the same vector and differ, which is why obligation
in this store is a log position rather than a vector. Both the cursor and the
outbox are then DERIVED from one column on the update rows,
MAX(authoritySeq)andauthoritySeq IS NULL, so neither can disagree with the bytes it accounts for (packages/data/evidence/invariants.test.ts, ADR-0298). - Use
Y.encodeStateVector(doc)to describe local clocks, thenY.encodeStateAsUpdateV2(doc, remoteStateVector)to send only missing updates. - Persist and transmit bytes from the
updateV2event. Replay them withY.applyUpdateV2(doc, update, origin). - Wrap multi-write user actions in
doc.transact(() => { ... }, origin). This reduces observer churn and gives persistence, providers, and undo logic a useful origin. - Treat transaction origins as the boundary for filtering provider echoes, app-authored operations, and undo tracking.
- Scope
Y.UndoManagerto concrete shared types. SettrackedOrigins, tunecaptureTimeout, and callstopCapturing()between logically separate commands. - Use relative positions for collaborative cursor and selection anchors. Raw numeric indexes drift under remote edits.
Y.snapshot()is a historical marker that depends on retained delete history.Y.encodeStateAsUpdateV2(doc)is the self-contained checkpoint format.- Prefer separate top-level docs over Yjs subdocuments unless Epicenter owns the whole provider lifecycle for the subdoc path.
Store Connection
- Yjs is network-agnostic. It supplies CRDT state, state vectors, updates, and awareness behavior, not Epicenter's connection topology, authorization, or durability contract.
- One socket per application, not one per open document. A replica connects to
STORE_SYNC_ROUTE.pattern(/api/store/v1/sync, inpackages/sync/src/store-route.ts) with anamespacenaming the workspace and acursornaming its own durably applied position, so a reconnect is a catch-up rather than a fresh start (ADR-0222). - Whose data it is never appears in the query. It comes from the resolved bearer, server-side, so there is no value a client can put in the URL that reaches another partition (ADR-0092).
- Browser upgrades authenticate through exactly one
bearer.<token>subprotocol entry, because a browser upgrade cannot setAuthorization; the mount echoes only the main subprotocol on the 101, so the token never round-trips. Non-browser clients may use anAuthorizationheader. Do not use cookie-only upgrades, query-string credentials, or post-accept authentication frames. - The wire is framing and nothing else:
push,ack,refuse,entry,offer,snapshot,wanted(packages/data/src/sync/frames.ts). No frame knows what an update means, what a row is, or what Yjs is, which is exactly why chunking is safe at that layer. - Large updates are chunked at
CHUNK_BYTES, set by Cloudflare's documented Durable Object SQLite value cap rather than by anything about Yjs. Do not raise it to the measured wall; the documented limit is the one Cloudflare is entitled to enforce. - Presence is deliberately absent until a concrete consumer earns awareness state and disconnect cleanup. If added later, awareness is ephemeral and must never be persisted into the Y.Doc or the SQLite update log as canonical data.
One Document Per Application
An application is ONE Y.Doc. Roots are tables:<name> and kv. A row is a
nested Y.Type attribute on its table root, a field holding a value is a
JSON attribute on the row, and the one field holding a node is a nested
Y.Type at the row's reserved content key (ADR-0295, ADR-0309). Holding the
attribute is what it means to exist; removing it is what deletion does, and it
reclaims the row's whole subtree in one operation.
Those two words are the vocabulary. A value is replaced whole on write, so two devices writing one converge on a winner. A node is edited in place, so two devices editing one both keep every keystroke.
<!-- vocab-check: ignore-next-line (naming what is retired) -->Their retired names are scalar and prose; both belong in no new code or documentation (ADR-0309).
The nesting is not stylistic. Item.write calls findRootTypeKey, a linear
scan of doc.share, so one root per row makes encoding quadratic in rows
(5,417 ms for 20,000 rows against 13 ms nested).
There are no independent row documents. A row's content node used to live in its own
top-level document at a derived address, with a document manager, a tombstone
table and an openDocument verb (ADR-0248); ADR-0295 collapsed all of it into
the row. Advice naming documents.ts, openDocument, _tombstones, or
"hydrate the row's document" is describing a design that no longer exists.
// Values are attributes on the row, written through the table.
db.tables.notes.update(noteId, { title, pinned: true });
// The content node is ON the row, read synchronously with everything else.
const note = db.tables.notes.get(noteId);
const body = note?.content; // a live Y.Type an editor binds to
Inside the application document only Doc.get mints, and every key reaching it
must be a table name the database declares: reading an unknown ROW through
getAttr costs nothing, while a misspelled TABLE name costs a permanent root.
Three Signals, And Which One Fires
table.subscribefires when a table's SHAPE changes: a row added, removed, or a value edited. It does NOT fire for an edit inside a content node. It hands the listener the ROW IDS the commit touched, so a consumer holding a projection rebuilds only what moved; a consumer that just re-reads may ignore them.table.watch(node)fires for edits inside one content node, keyed by the node's own identity.kv.subscribefires when any declared key changes, and carries nothing. There are ten keys, so naming them would buy nothing.
The distinction is forced by the library. Delivery routes off
transaction.changed, which Yjs fills with the types a transaction modified
DIRECTLY, so a keystroke in a body puts the BODY's type there; its parent is
the row, not the table root. Nothing bubbles to the table. A surface that
watches a table for changes inside a node sees nothing.
Owner-Side Persistence
One document, so one chain: _updates (id, bytes, authoritySeq) in SQLite, and
the matching object store in the browser's IndexedDB. There is no per-document
partition, no _tombstones, and no separate _outbox — what a replica still
owes is the rows with authoritySeq IS NULL, which is a partial index rather
than a second table (ADR-0238). Do not add a separate IndexedDB provider or a
second document store.
- Hydrate BEFORE attaching the
updateV2listener. Replaying stored bytes through the listener would re-append them; the engine applies its history first and then attaches, and throws if a foreign apply ever reaches the listener, so a mistake here fails the open loudly rather than duplicating a log (packages/data/src/store/store.ts). - A locally authored append joins the durable queue owed. Authority-accepted bytes arrive on a remote origin and create no outbound obligation, which is what the one listener checks before appending.
- The chain compacts by ROW, and the row's
authoritySeqpicks which of two mechanisms applies (ADR-0301). Rows the authority has taken replay into a freshgc: truedocument and rewrite as one complete V2 state update: replay rather thanmergeUpdatesV2, because merging does not GC and collapsing tombstones is the point.encodeStateAsUpdateV2folds buffered pending state back into its output, so a fold taken while dependencies are missing cannot silently drop them. - Rows still OWED cannot take that path, and this is the one to get right. A
whole-document re-encode is not a delta the authority could be offered, so
owed rows collapse with
mergeUpdatesV2into one resendable row that takes a NEW id above every existing one. Folding them like acknowledged rows would offer the authority a whole document per keystroke; inheriting a lower id
Content truncated.
When not to use it
- →When high-latency operations do not require real-time conflict resolution
- →When local-only, single-user state management is sufficient
Prerequisites
Limitations
- →High memory consumption with large, unoptimized document state
- →Requires explicit handling of provider-specific lifecycle
How it compares
It provides structural guidance for distributed state, avoiding the common pitfalls of manual merge-conflict resolution.
Compared to similar skills
yjs side by side with the closest alternatives in the catalog.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| yjs (this skill) | 2 | 3mo | No flags | Advanced |
| shopify-development | 12 | 8mo | Review | Intermediate |
| nuxt | 19 | 8mo | No flags | Intermediate |
| tanstack-query | 7 | 3mo | No flags | Intermediate |
Try saying
Example prompts that trigger this skill in your AI assistant.
More by EpicenterHQ
View all by EpicenterHQ →You might also like
shopify-development
davila7
Build Shopify apps, extensions, themes using GraphQL Admin API, Shopify CLI, Polaris UI, and Liquid. TRIGGER: "shopify", "shopify app", "checkout extension", "admin extension", "POS extension", "shopify theme", "liquid template", "polaris", "shopify graphql", "shopify webhook", "shopify billing", "app subscription", "metafields", "shopify functions"
nuxt
antfu
Nuxt full-stack Vue framework with SSR, auto-imports, and file-based routing. Use when working with Nuxt apps, server routes, useFetch, middleware, or hybrid rendering.
tanstack-query
exceptionless
Data fetching and caching with TanStack Query in Svelte. Query patterns, mutations, cache invalidation, WebSocket-driven updates, and optimistic updates. Keywords: createQuery, createMutation, TanStack Query, query keys, cache invalidation, optimistic updates, refetch, stale time, @exceptionless/fetchclient, WebSocket
telegram-dev
2025Emma
Telegram 生态开发全栈指南 - 涵盖 Bot API、Mini Apps (Web Apps)、MTProto 客户端开发。包括消息处理、支付、内联模式、Webhook、认证、存储、传感器 API 等完整开发资源。
bun-development
davila7
Modern JavaScript/TypeScript development with Bun runtime. Covers package management, bundling, testing, and migration from Node.js. Use when working with Bun, optimizing JS/TS development speed, or migrating from Node.js to Bun.
shopify-apps
alinaqi
Shopify app development - Remix, Admin API, checkout extensions