{"id":16,"date":"2026-07-31T18:31:32","date_gmt":"2026-07-31T18:31:32","guid":{"rendered":"https:\/\/blog.amitdhiman.com\/?p=16"},"modified":"2026-07-31T18:31:32","modified_gmt":"2026-07-31T18:50:02","slug":"tech-local-first-collaboration-yjs","status":"publish","type":"post","link":"https:\/\/blog.amitdhiman.com\/?p=16","title":{"rendered":"Local-First Collaboration with Yjs: Design Beyond the Shared Editor"},"content":{"rendered":"<p>A field team writing inspection notes cannot depend on a permanent network connection. People should be able to type immediately, close the application, and synchronize later. For frontend engineers building collaborative documents, Yjs provides a useful foundation for this behavior. The challenge is designing the surrounding application so that a successful merge also makes sense to its users.<\/p>\n<h2>Separate convergence from product correctness<\/h2>\n<p>Yjs represents shared data with conflict-free replicated data types, or CRDTs. Its binary updates can arrive out of order or more than once. When replicas receive all relevant updates, they converge. The <a href=\"https:\/\/docs.yjs.dev\/api\/document-updates\">official update API documentation<\/a> explains these properties and the state vectors used to exchange missing changes.<\/p>\n<p>This is established functionality, with editor bindings and persistence providers already available. The broader local-first application model remains an architectural choice with evolving patterns for authorization, schema changes, and deployment. Installing a CRDT library does not automatically make an entire product usable offline.<\/p>\n<p>For inspection notes, simultaneous text edits are a reasonable fit. A rule that only one technician may reserve the final replacement part is different: two offline users can each believe they succeeded. Keep inventory allocation authoritative or design an explicit reconciliation process. Convergence cannot independently enforce every business invariant.<\/p>\n<h2>Prove the merge before adding a network<\/h2>\n<p>You need basic JavaScript, a supported Node.js installation with npm, and later a browser application with a bundler. Start in a disposable project with <code>npm install yjs<\/code>. Save this example as an ES module and run it with Node. It creates two replicas, gives them a common starting state, and exchanges changes made independently:<\/p>\n<pre><code>import * as Y from 'yjs';\n\nconst office = new Y.Doc();\nconst field = new Y.Doc();\noffice.getText('notes').insert(0, 'Inspect pump.');\nY.applyUpdate(field, Y.encodeStateAsUpdate(office));\n\noffice.getText('notes').insert(0, 'Office: ');\nconst fieldText = field.getText('notes');\nfieldText.insert(fieldText.length, ' Check seal.');\n\nconst officeUpdate = Y.encodeStateAsUpdate(office);\nconst fieldUpdate = Y.encodeStateAsUpdate(field);\nY.applyUpdate(office, fieldUpdate);\nY.applyUpdate(field, officeUpdate);\n\nconsole.log(office.getText('notes').toString());\nconsole.log(field.getText('notes').toString());<\/code><\/pre>\n<p>Both outputs should contain the prefix, original note, and appended instruction. This example transfers full state for clarity. Repeat an update to check idempotence. Then experiment with both users inserting at the same position: the replicas still agree, but the resulting sentence may require human editing.<\/p>\n<h2>Give the browser durable local state<\/h2>\n<ol>\n<li>Create one Yjs document per logical inspection and assign it a stable identifier. Use shared types such as <code>Y.Text<\/code> for collaborative content.<\/li>\n<li>Add <code>y-indexeddb<\/code> and construct <code>IndexeddbPersistence<\/code> with an account-scoped document key and the document. Wait for its initial synchronization before deciding whether local content exists.<\/li>\n<li>Bind a supported editor to the shared text through its Yjs integration. Avoid replacing the whole shared value on every keystroke.<\/li>\n<li>Add <code>y-websocket<\/code> against a server you operate, using the same logical document identifier. Configure server persistence and authorization deliberately.<\/li>\n<li>Cache the application shell with a service worker if users must reopen it offline. Persisted document content alone cannot load missing JavaScript or styles.<\/li>\n<\/ol>\n<p>The <a href=\"https:\/\/docs.yjs.dev\/getting-started\/allowing-offline-editing\">offline support guide<\/a> covers local persistence, while the <a href=\"https:\/\/docs.yjs.dev\/ecosystem\/connection-provider\/y-websocket\">WebSocket provider guide<\/a> explains network synchronization. Start from the server package instructions matching your installed version; package layouts can change.<\/p>\n<h3>Use status language that users can trust<\/h3>\n<p>Distinguish an edit reflected in memory, local persistence completion, and server durability. A connected socket is not proof that a backup exists. If the provider does not expose the durability acknowledgement your product needs, add an application-level acknowledgement rather than labeling every connection event as saved.<\/p>\n<p>Also test with separate browser profiles. Same-browser tabs can communicate through local channels, masking a broken network path. Disconnect two profiles, edit both, reconnect, reload, and restart the synchronization server. Verify content after every transition.<\/p>\n<h2>Protect documents and plan their lifetime<\/h2>\n<p>A room name is an identifier, not an access control mechanism. Authenticate connections and authorize each document on the server. Use secure WebSockets in production, validate origins where appropriate, and enforce size and rate limits. Keep presence information separate from durable document content.<\/p>\n<p>Local storage exposes additional copies on shared devices. Clearing an account session does not automatically erase IndexedDB, and revoking access cannot recall data already copied offline. Explain retention, implement account-scoped cleanup, and treat encryption and key management as separate design work.<\/p>\n<p>Keep a small compatibility fixture containing documents created by an older application release. Open it with the new release and simulate an older client reconnecting afterward. This catches schema assumptions that a two-tab demonstration misses. Decide whether incompatible clients receive an upgrade prompt, a read-only view, or a separate migration path.<\/p>\n<p>Budget for synchronization hosting, backups, document growth, and schema compatibility. Browser storage can be cleared or evicted, so provide export and recovery paths. Your next milestone should be one inspection workflow that survives offline reloads and reconnects, with understandable permissions and recovery behavior.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Use Yjs to explore offline edits and convergence, then plan the persistence, permissions, and product behavior a dependable collaborative application needs.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[8,5],"tags":[],"class_list":["post-16","post","type-post","status-publish","format-standard","hentry","category-data-security","category-digital-world"],"_links":{"self":[{"href":"https:\/\/blog.amitdhiman.com\/index.php?rest_route=\/wp\/v2\/posts\/16","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.amitdhiman.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.amitdhiman.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.amitdhiman.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.amitdhiman.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=16"}],"version-history":[{"count":1,"href":"https:\/\/blog.amitdhiman.com\/index.php?rest_route=\/wp\/v2\/posts\/16\/revisions"}],"predecessor-version":[{"id":34,"href":"https:\/\/blog.amitdhiman.com\/index.php?rest_route=\/wp\/v2\/posts\/16\/revisions\/34"}],"wp:attachment":[{"href":"https:\/\/blog.amitdhiman.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=16"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.amitdhiman.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=16"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.amitdhiman.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=16"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}