#auto-merge #crdt #serde-derive #sync

automorph

Derive macros for bidirectional Automerge-Rust struct synchronization

1 unstable release

Uses new Rust 2024

new 0.1.0 Feb 7, 2026

#368 in Encoding

Apache-2.0 OR MIT

460KB
10K SLoC

Automorph

Crates.io Documentation License

Bidirectional synchronization between Rust types and Automerge documents.

Automorph works like Serde - derive a trait on your structs and the library handles synchronization automatically. Unlike serialization, Automorph performs efficient diff-based updates, only writing changes to the Automerge document.

All features documented below are validated by automated tests. Test names are noted in parentheses.

What is Automerge?

Automerge is a Conflict-free Replicated Data Type (CRDT) library that enables automatic merging of concurrent changes without coordination. It's ideal for:

  • Local-first software: Apps that work offline and sync when connected
  • Real-time collaboration: Multiple users editing the same document
  • Version control for data: Full history with time-travel debugging

Quick Start

(validated by: test_derived_struct)

Add to your Cargo.toml:

[dependencies]
automorph = "0.1"
automerge = "0.7"

Then derive Automorph on your types:

use automorph::{Automorph, Result};
use automerge::{AutoCommit, ROOT};

#[derive(Automorph, Debug, PartialEq, Default, Clone)]
struct Person {
    name: String,
    age: u64,
}

fn main() -> Result<()> {
    // Create an Automerge document
    let mut doc = AutoCommit::new();

    // Save a struct to the document
    let person = Person {
        name: "Alice".to_string(),
        age: 30,
    };
    person.save(&mut doc, &ROOT, "person")?;

    // Load it back
    let restored = Person::load(&doc, &ROOT, "person")?;
    assert_eq!(person, restored);

    // Efficient updates - only changed fields are written
    let mut updated = person.clone();
    updated.age = 31;
    updated.save(&mut doc, &ROOT, "person")?; // Only writes the age change

    Ok(())
}

Features

Efficient Diff-Based Updates

(validated by: test_diff_detects_changes, test_diff_no_changes_when_equal)

Automorph only writes values that have changed:

person.age = 31;
person.save(&mut doc, &ROOT, "person")?;
// Only the 'age' field generates an Automerge operation

Version-Aware Operations

(validated by: test_derived_version_aware, test_diff_versions)

Access historical versions with *_at methods:

// Save state
person.save(&mut doc, &ROOT, "person")?;
let checkpoint = doc.get_heads();

// Make changes
person.age = 32;
person.save(&mut doc, &ROOT, "person")?;

// Load from checkpoint (time travel!)
let old_person = Person::load_at(&doc, &ROOT, "person", &checkpoint)?;
assert_eq!(old_person.age, 31);

Change Detection

(validated by: test_update_returns_change_report, test_hierarchical_change_tracking, test_change_report_paths)

Detect and inspect changes with update and diff:

// Update returns a ChangeReport showing what changed
let changes = person.update(&doc, &ROOT, "person")?;
if changes.any() {
    println!("Changed fields: {:?}", changes.leaf_paths());
}

Comprehensive Type Support

Automorph supports all types that Serde does:

  • Primitives: bool, i8-i128, u8-u128, f32, f64, char
  • Strings: String, &str, Box<str>, Cow<str>
  • Collections: Vec, HashMap, BTreeMap, HashSet, BTreeSet
  • Options: Option<T>, Result<T, E>
  • Tuples: up to 16 elements
  • Smart pointers: Box, Rc, Arc, Cell, RefCell
  • Time: Duration, SystemTime
  • Network: IpAddr, SocketAddr
  • And more: ranges, paths, NonZero types...

Derive Macro Attributes

Container Attributes

(validated by: test_rename_all, test_internally_tagged_enum)

#[derive(Automorph)]
#[automorph(rename_all = "camelCase")]  // Rename all fields
struct Config {
    user_name: String,  // Stored as "userName"
}

#[derive(Automorph)]
#[automorph(tag = "type")]  // Internal tagging for enums
enum Message {
    Text { content: String },
    Image { url: String },
}
// Stored as: {"type": "Text", "content": "Hello"}

Field Attributes

(validated by: test_field_rename, test_skip_field, test_default_field)

#[derive(Automorph)]
struct User {
    #[automorph(rename = "id")]
    user_id: u64,

    #[automorph(skip)]           // Don't sync this field
    cache: Option<Vec<u8>>,

    #[automorph(default)]        // Use Default if missing
    score: u32,
}

Comparison with Serde

Feature Serde Automorph
Derive macro #[derive(Serialize, Deserialize)] #[derive(Automorph)]
Field rename #[serde(rename)] #[automorph(rename)]
Skip field #[serde(skip)] #[automorph(skip)]
Default value #[serde(default)] #[automorph(default)]
Bidirectional Needs both traits Single trait
Diff-based No Yes - only writes changes
Change tracking No Yes - diff() and update()
CRDT-aware No Yes - preserves Automerge semantics

Documentation

Examples & Demo

Learn Automorph through hands-on examples:

  • Examples - Single-file examples covering individual features:

    • collaborative.rs - Tracked, versioning, change detection
    • crdt_collaboration.rs - Counter and Text CRDT types
    • persistence.rs - File storage patterns
    • sync_tcp.rs - TCP sync protocol
    • notes_tutorial.rs - Complete CLI notes app
  • Demo Application - Full-featured Yew/WASM web application:

    • Collaborative todo list and chat
    • WebSocket sync between browsers
    • Docker Compose deployment
    • Production-ready patterns

Start with the examples to learn individual concepts, then study the demo for production patterns.

License

Automorph is dual-licensed under:

You may use this software under either license at your option.

  • Automerge
  • Autosurgeon -- Automorph provides granular change tracking vs. Autosurgeon's serialization/deserialization functionality.

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for guidelines.

Dependencies

~9–13MB
~183K SLoC