I abused JS to abuse Yjs A developer built Plexus, an open-source library that lets existing TypeScript class-based object models become collaborative and local-first by wrapping Yjs CRDTs behind MobX-style decorators, rather than rewriting the application around Yjs data structures. The project was motivated by a model spanning millions of lines of code that could not be rewritten, so the approach preserves class behavior by changing only the decorators used. The core package was written by hand, with AI assistance limited to tests, auxiliary packages, and documentation structuring. Because I just wanted to keep my TypeScript classes. I was not trying to build a CRDT framework. Honestly, I really, really wanted to avoid building one, and I guess you can see that in the API. I ended up creating Plexus https://plexus.here.build/ https://plexus.here.build/ , https://github.com/here-build/plexus https://github.com/here-build/plexus . I had a fairly large TypeScript object model. It originally grew out of the open-source model by third party, and it already had a mental model I liked: classes, inheritance, object references, Maps, Sets, arrays, and MobX for reactivity. Then I needed it to become collaborative and local-first. The obvious answer was Yjs. The less obvious problem was that I really, really didn’t want to rewrite the application around Yjs data model; I wanted to keep using the same classes. Disclaimer on AI usage inside Plexus: Package core was fully written manually; tests and small amount of latter additions like extra methods on data structures were AI-assisted, but the core was 100% hand-crafted. Examples and some auxiliary packages like y-messageport were mostly AI-written and reviewed thoroughly afterwards. Documentation was originally handwritten; AI only participated in structuring it and proofreading. Yjs is a CRDT engine. CRDT, explained very roughly: instead of keeping one authoritative copy of your state, it gives you shared data structures whose changes can be made independently on different peers and later merged without everybody having to serialize their writes through one server. Alice sets foo.bar = 1 , Bob sets foo.baz = 2 , both get {bar: 1, baz: 2} . Things get messy with arrays, but conceptually it’s working this way. It gives you things like Y.Map , Y.Array , Y.Text , transactions, events, update encoding, and all the machinery needed to make replicated state actually converge. That’s great. But if you use those structures directly, they very quickly become your application model. Instead of this: class Component { name = ""; children: Component = ; } you start writing something more like js const component = new Y.Map ; component.set "name", "" ; const children = new Y.Array ; component.set "children", children ; And now everywhere that touches your model needs to know that name lives in a Y.Map , that children is a Y.Array , how nested values are materialized, what an observer receives, when to transact, how references are represented, and so on. And yes, the API and behavior of them differ from native objects. There is nothing wrong with that API; I just already had an application model, and I didn’t want another one. The model I had was already built around classes and MobX decorators. It looked roughly like this: class DataSourceDefinition implements HasLineage { @observable name: string; @observable label?: string; @observable description?: string; @observable category?: string; @observable type: "query" | "mutation" | CustomFunction | DataQueryFetch; @observable papp: PartialApplication; @observable extends?: DataSourceDefinition; @observable.shallow invalidates: DefinitionInvalidationTarget = ; @observable.shallow externalLinks: Record