WeaveDocs

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-sdk crate available — see Weave SDK overview.

Build the app

  1. 1
    Scaffold the project

    Create a new Rust binary crate.

    cargo new --bin shared-notes
    cd shared-notes
  2. 2
    Declare dependencies

    Add weave-sdk for the node runtime, plus tokio and anyhow.

    [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"
  3. 3
    Write the node

    The full src/main.rs builds a WeaveNode, opens a Locus drive named notes, 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(())
    }
  4. 4
    Run 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-b

    Both nodes discover each other through start_network and start_auto_replication.

  5. 5
    Exchange 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
  6. 6
    Verify crash safety

    Kill node-a with Ctrl-C and bring it back. Its notes drive is intact — read meeting.txt still works because Locus journal replay reconstructs state on open.

How the primitives compose

  • The Strand holds the file-operation journal: every write, mkdir, remove is 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:

  1. A new Strand Blobs entry holding the file contents.
  2. A new journal block referencing the blob id.
Note

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.