Kevin Jahns
Yjs Workshop
BOB 2025
Single User Applications
- VSCode
- LibreOffice
- Inkscape

=> everything we do nowadays is collaborative
Web Applications
- Wordpress
- Drupal
- ..









Conflict Resolution: Last Writer Wins
Conflict Resolution: Duplication




Manual Conflict Resolution

Avoid Conflicts Through Coordination
- Slack, Matrix
- Only users assigned to a ticket may manipulate
- WordPress Workflow: There is a dedicated person who writes content into WordPress
Realtime & Automatic Conflict Resolution

We can make anything collaborative..

Agenda
- Yjs data types
- Sync Yjs documents
- Use Yjs extensions
- collaborative react applications?
Goal: collaborative, local-first (note-taking, drawing, ..) app
git clone https://github.com/yjs/bob-2025
Yjs
- observable data types
- Y.Map
- Y.Array
- Y.Text
- Y.Xml*
- automatic conflict resolution
- network agnostic
Shared Data Types
import * as Y from 'yjs'
const ydoc = new Y.Doc()
const ymap = ydoc.getMap('map')
// const yarray = ydoc.getArray('array')
// const ytext = ydoc.getText('text')
// const yxml = ydoc.getXmlElement('el')
ymap.observe(event => {
console.log(event.changes.keys)
})
ymap.set('my key', 'new value') // => { "my key": { action: 'add', oldValue: undefined } }
Exercise:
- Observe a Y.Array<number>
- If it has more than 10 elements, delete the smallest element.
Bonus: build a counter CRDT
Yjs always syncs
Updates contain a SET of operations
- ydoc1 => { op1, op2 }
- ydoc2 => { op2, op3 }
- merge(ydoc1, ydoc2) => { op1, op2, op3 }
Yjs always syncs
import * as Y from 'yjs'
const ydoc1 = new Y.Doc()
const ymap1 = ydoc1.getMap()
ymap1.set('a', 1)
const ydoc2 = new Y.Doc()
const ymap2 = ydoc2.getMap()
ymap2.set('b', 2)
Y.applyUpdate(ydoc2, Y.encodeStateAsUpdate(ydoc1))
Y.applyUpdate(ydoc1, Y.encodeStateAsUpdate(ydoc2))
console.log(ymap1.toJSON()) // => { a: 1, b: 2 }
console.log(ymap2.toJSON()) // => { a: 1, b: 2 }
Sync only the differences
import * as Y from 'yjs'
const ydoc1 = new Y.Doc()
const ymap1 = ydoc1.getMap()
ymap1.set('a', 1)
const ydoc2 = new Y.Doc()
const ymap2 = ydoc2.getMap()
ymap2.set('b', 2)
// Sync the differences from ydoc2 into ydoc1
// The state vector describes what ydoc1 contains
const syncStep1 = Y.encodeStateVector(ydoc1)
// Only encode the differences into a update message
const syncStep2 = Y.encodeStateAsUpdate(ydoc2, syncStep1)
// Apply the differences to ydoc1
Y.applyUpdate(ydoc1, syncStep2)
console.log(ymap1.toJSON()) // => { a: 1, b: 2 }
Exercise 2: Sync two Yjs documents
- Create two Yjs documents
- Listen to `update` events and apply the update on the other Yjs document.
=> Immediately continue with providers
Providers
- y-websocket
- y-indexeddb
- y-webrtc
Exercise:
- Add y-websocket to one of your documents from the previous exercise
- Room name: "bob-2025"
Use inspect.yjs.dev
Use Bindings
y-quill setup
+ possibly set up indexeddb
React with SyncedStore

SyncedStore
import React from "react";
import { useSyncedStore } from "@syncedstore/react";
import { store } from "./store";
export default function App() {
const state = useSyncedStore(store);
return (
<div>
<p>Todo items:</p>
<ul>
{state.todos.map((todo, i) => (
<li key={i} style={{ textDecoration: todo.completed ? "line-through" : "" }}>
<label>
<input type="checkbox" checked={todo.completed} onClick={() => (todo.completed = !todo.completed)} />
{todo.title}
</label>
</li>
))}
</ul>
<input
onKeyPress={(event) => {
if (event.key === "Enter") {
state.todos.push({ completed: false, title: event.target.value });
target.value = "";
}
}}
/>
</div>
);
}
SyncedStore

Relm.us

Yjs Ecosystem
- y-webrtc
- y-websocket
- y-redis
- hocuspocus
- liveblocks
- y-sweet
- yrs-warp
- matrix-crdt
- ..
- y-prosemirror
- y-quill
- y-codemirror
- y-ace
- y-monaco
- lexical
- Rows n' Columns
- ...
Editor Bindings
Connectors
Persistence
- y-indexeddb
- y-leveldb
- ystream
Cloud Providers
- Affine Cloud
- Liveblocks
- y-sweet Cloud
- Hocuspocus Cloud
- Partykit / Cloudflare
Y CRDT
Yjs ported to different programming languages






Ycs
Exercise
- Start one of the examples in the `bob-2025` repository
- Add y-indexeddb
- inspect the structure of the document
Tutorial: How to break a CRDT :)
Local-First business appeal
- Quick prototyping
- Realtime & collaborative
- Little setup
- No backend logic
- (Pay as you go)
- (snapshots)
- (offline support)
Yjs in practice
- You have an existing application
=> Sync to Yjs types
- Existing communication protocol
=> Implement provider
- Auth
- Existing database with documents.
=> Store Yjs docs in a database
Control
Control
Image is CC: https://www.flickr.com/photos/nossreh/10987353554
Data
Conflict Resolution

Bonus exercises
- Implement an efficient Counter CRDT (addition and subtraction)
Centrality
source: https://www.printablee.com/post_50-states-printable-out-maps_185196/

Centrality


Why can't my favorite file syncing service sync directly to my phone. Instead I first upload to the US, and then sync back
Centrality / Central Place of Control
+ Easier to coordinate
- No direct sync
- No sync if service is not reachable
- Harder to scale
Conflict-free RDTs: CRDTs
Alternatively: Coordination-free RDT
I.e. RDTs that don't require centrality


Shared Control
Data
Conflict Resolution
Sync Peer-to-Peer



Sync on the Edge





Make it Available Offline








twitter.com/kevin_jahns

Bob 2025
By Kevin Jahns
Bob 2025
- 69