Nostr CRDT

npm version Coverage Status

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:

screencapture Open live demo

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

GitHub

View Github