A collaborative Rich Text editing experience (similar to Google Docs) using nostr-crdt
Nostr CRDT
nostr-crdt is an experiment to run decentralized, collaborative (multiplayer) apps over nostr. CRDT application updates are sent as Nostr events.
The NostrProvider is a sync provider for Yjs, a proven, high performance CRDT implementation.
TL;DR
Create apps like this and run them over Nostr:
Live demo
In the examples directory, you’ll find some live examples:
Summary of how it works
When using CRDTs (Conflict-free Replicated Data Types), you don’t need to store “the current application state” in a central database. Instead, the state is derived from all updates that have been made.
Nostr-crdt shares these updates using the Nostr protocol as events. Instead of social updates or chat messages (main use-case for nostr), we send an event stream of data model updates (for the rich-text demo for example, updates are “rich-text document edits”, for the TODO-list examples, updates are the creation or completion of todo items) over the Nostr protocol.
The main code to create a simple, collaborative TODO list on top of nostr-crdt is < 100 lines (see App.tsx).
An initial event is created to define a “room” (like a document or todo-list). Updates to this room are sent by creating nostr events with an #e
tag to the initial event id (room id).
Updates could be spread across relays, or stored locally in clients and synced at a later moment.
Usage
nostr-crdt currently works with Yjs or SyncedStore.
Usage with Yjs
To setup nostr-crdt, 3 steps are needed:
- Create a Yjs
Y.Doc
- Connect to a relay using nostr-tools
- Create and initialize your nostr-crdt
NostrProvider
import { NostrProvider, createNostrCRDTRoom } from "nostr-crdt";
import { generatePrivateKey, relayInit } from "nostr-tools";
import * as Y from "yjs";
const nostrClient = relayInit("wss://nostr-url");
await nostrClient.connect();
const key = generatePrivateKey();
const ydoc = new Y.Doc();
// Send a first event using Nostr to create a new "room"
// (not necessary when joining an existing room)
const roomId = await createNostrCRDTRoom(doc, nostrClient, key, "demo");
// Create and connect the NostrProvider to the Y.Doc
const nostrProprovidervider = new NostrProvider(
doc,
client,
key,
roomId,
"demo"
);
await provider.initialize();
// array of numbers which produce a sum
const yarray = ydoc.getArray("count");
// observe changes of the sum
yarray.observe((event) => {
// print updates when the data changes
console.log("new sum: " + yarray.toArray().reduce((a, b) => a + b));
});
// add 1 to the sum
yarray.push([1]); // => "new sum: 1"
Local-first
Note that you don’t need to have a connection to a Relay for the demo apps to work. With nostr-crdt you can build local-first apps, and sync over nostr as soon as you’re back online.
Read more about the benefits of Local-first software in this essay
Future work
The current state is a proof of concept to gather community feedback. Brainstorm of future work necessary:
- a NIP for events (we now use kind=9001)
- periodic snapshots of events
- how to handle long-term storage, do we need specific relays / NIPs for this?
- design for access control (who can collaborate on the same “document” / room)
- send presence / cursor info as ephemeral events
- buffer stored events every x seconds, and send “live” data as ephemeral events to reduce network load