Shared Notes
Build a crash-safe collaborative notes app with Strand, Locus, and weave-dht.
What you'll build
A small Rust binary that runs as a node on the Weave network. Each node hosts a Locus drive named notes. Edits made on any node propagate to the others. Crashes recover automatically.
Time: 15 minutes.
Primitives used: Strand (signed history of edits), Locus (distributed filesystem), weave-dht (peer discovery).
Prerequisites
- Rust 1.78+ (
rustup default stable). - Two terminals (you'll run two nodes locally).
- The
weave-sdkcrate available — see Weave SDK overview.
Build the app
- 1Scaffold the project
Create a new Rust binary crate.
cargo new --bin shared-notes cd shared-notes - 2Declare dependencies
Add
weave-sdkfor the node runtime, plustokioandanyhow.[package] name = "shared-notes" version = "0.1.0" edition = "2021" [dependencies] weave-sdk = "1.1.0" tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-std", "io-util"] } anyhow = "1" - 3Write the node
The full
src/main.rsbuilds aWeaveNode, opens a Locus drive namednotes, and exposes a small REPL.use anyhow::{Context, Result}; use std::env; use tokio::io::{AsyncBufReadExt, BufReader}; use weave_sdk::prelude::*; #[tokio::main] async fn main() -> Result<()> { let args: Vec<String> = env::args().collect(); let id = args .get(1) .cloned() .unwrap_or_else(|| "node-a".to_string()); let storage = format!("/tmp/weave-{id}"); let node = WeaveNode::builder() .namespace("notes-demo") .identifier(&id) .storage_dir(&storage) .build() .await .with_context(|| format!("failed to build node {id}"))?; node.start_network().await?; node.start_auto_replication().await?; println!("== node {id} =="); println!("did: {}", node.identity().did()); println!("storage: {storage}"); node.open_locus("notes").await?; node.locus_mkdir("notes", "/").await.ok(); // tolerate "exists" println!(); println!("commands:"); println!(" write <path> <content...> - write a note"); println!(" read <path> - read a note"); println!(" ls [path] - list a directory"); println!(" peers - show peer count"); println!(" quit - exit"); println!(); let stdin = tokio::io::stdin(); let mut lines = BufReader::new(stdin).lines(); while let Some(line) = lines.next_line().await? { let cmd: Vec<&str> = line.splitn(3, ' ').collect(); match cmd.as_slice() { ["write", path, body] => { let p = ensure_leading_slash(path); node.locus_write_file("notes", &p, body.as_bytes()).await?; println!("wrote {p} ({} bytes)", body.len()); } ["read", path] => { let p = ensure_leading_slash(path); match node.locus_read_file("notes", &p).await { Ok(bytes) => println!("{}", String::from_utf8_lossy(&bytes)), Err(e) => eprintln!("read failed: {e}"), } } ["ls"] => print_dir(&node, "/").await?, ["ls", path] => print_dir(&node, &ensure_leading_slash(path)).await?, ["peers"] => println!("peers: {}", node.peer_count().await), ["quit"] => break, ["help"] | [""] => {} _ => eprintln!("unknown command: {line}"), } } Ok(()) } fn ensure_leading_slash(p: &str) -> String { if p.starts_with('/') { p.to_string() } else { format!("/{p}") } } async fn print_dir(node: &WeaveNode, path: &str) -> Result<()> { let entries = node.locus_readdir("notes", path).await?; for e in entries { println!(" {e}"); } Ok(()) } - 4Run two nodes
Open two terminals. Each runs the same binary with a different identifier.
# terminal 1 cargo run -- node-a# terminal 2 cargo run -- node-bBoth nodes discover each other through
start_networkandstart_auto_replication. - 5Exchange a note
# in node-a > write meeting.txt Tomorrow 10am sync with the platform team wrote /meeting.txt (43 bytes) # in node-b, after a moment > ls meeting.txt > read meeting.txt Tomorrow 10am sync with the platform team - 6Verify crash safety
Kill
node-awith Ctrl-C and bring it back. Itsnotesdrive is intact —read meeting.txtstill works because Locus journal replay reconstructs state on open.
How the primitives compose
- The Strand holds the file-operation journal: every
write,mkdir,removeis a signed block. - Strand Blobs stores the file contents addressed by hash; multiple files referencing the same content dedupe automatically.
- Locus is the filesystem facade: a path tree mapped onto journal entries and blob ids.
- weave-dht lets the two nodes find each other and exchange Strand blocks.
Crash safety
Locus uses a journal-based design. Every write_file produces:
- A new Strand Blobs entry holding the file contents.
- A new journal block referencing the blob id.
If the process crashes between the two writes, the next open_locus runs journal replay and discards the half-finished operation. The previous version of the file is intact.
What to do next
- Add a watcher that prints incoming edits in real time: see Locus streams and watchers.
- Encrypt notes by enabling
Strand::with_encryption()on the underlying Strand. - Move beyond a CLI: pair this backend with the dBrowser for a desktop notes app.
- Try the P2P Chat tutorial — same Strand foundation, end-to-end encrypted messaging.