Getting Started with Grove
View SourceGrove helps you build apps where data syncs automatically—even when users edit the same thing at the same time, even when they're offline.
No conflict resolution dialogs. No "your changes were overwritten." It just works.
Installation
Add grove to your dependencies in mix.exs:
def deps do
[
{:grove, "~> 0.1.0"}
]
endThen run:
mix deps.get
Your First Tree (2 minutes)
Grove stores data in trees—perfect for forms, documents, outlines, or any hierarchical data.
# Start an interactive session
iex -S mix
# Create a tree with a unique replica ID
tree = Grove.Tree.new("my_laptop")
# Add some nodes
node1 = %Grove.Node{id: "task_1", type: :task, attrs: %{title: "Buy groceries"}}
node2 = %Grove.Node{id: "task_2", type: :task, attrs: %{title: "Walk the dog"}}
tree = tree
|> Grove.Tree.put_node(node1)
|> Grove.Tree.put_node(node2)
# Get a node
Grove.Tree.get_node(tree, "task_1")
# => %Grove.Node{id: "task_1", type: :task, attrs: %{title: "Buy groceries"}, ...}
# Update a node
tree = Grove.Tree.update_node(tree, "task_1", fn node ->
%{node | attrs: Map.put(node.attrs, :done, true)}
end)That's it! You have a working tree.
The Magic: Automatic Merge (3 minutes)
Here's where Grove shines. Let's simulate two devices editing the same data:
sequenceDiagram
participant A as Device A (Laptop)
participant B as Device B (Phone)
Note over A,B: Both start with same data
A->>A: Add "task_from_laptop"
B->>B: Add "task_from_phone"
Note over A,B: Devices are offline/concurrent
A->>B: Sync operations
B->>A: Sync operations
Note over A,B: Both now have ALL changes!# Device A: Create a tree
tree_a = Grove.Tree.new("laptop")
tree_a = Grove.Tree.put_node(tree_a, %Grove.Node{
id: "note_1",
type: :note,
attrs: %{text: "Original note"}
})
# Device B: Gets the same starting point
tree_b = Grove.Tree.new("phone")
tree_b = Grove.Tree.put_node(tree_b, %Grove.Node{
id: "note_1",
type: :note,
attrs: %{text: "Original note"}
})
# Now they BOTH edit at the same time (simulating offline/concurrent edits)
# Device A adds a task
tree_a = Grove.Tree.put_node(tree_a, %Grove.Node{
id: "task_from_laptop",
type: :task,
attrs: %{title: "Added on laptop"}
})
# Device B adds a different task
tree_b = Grove.Tree.put_node(tree_b, %Grove.Node{
id: "task_from_phone",
type: :task,
attrs: %{title: "Added on phone"}
})
# Get the pending operations from each device
{tree_a, ops_a} = Grove.Tree.flush_pending_ops(tree_a)
{tree_b, ops_b} = Grove.Tree.flush_pending_ops(tree_b)Now the magic—sync them:
# Device A receives Device B's changes
tree_a = Enum.reduce(ops_b, tree_a, fn {_id, event}, acc ->
case Grove.Tree.apply_remote(acc, event) do
{:ok, updated} -> updated
{:duplicate, existing} -> existing
end
end)
# Device B receives Device A's changes
tree_b = Enum.reduce(ops_a, tree_b, fn {_id, event}, acc ->
case Grove.Tree.apply_remote(acc, event) do
{:ok, updated} -> updated
{:duplicate, existing} -> existing
end
end)
# Check: Do they have the same nodes?
Map.keys(tree_a.nodes) |> Enum.sort()
# => ["note_1", "task_from_laptop", "task_from_phone"]
Map.keys(tree_b.nodes) |> Enum.sort()
# => ["note_1", "task_from_laptop", "task_from_phone"]
# Both devices now have ALL the changes!No conflicts. No data loss. They just merged.
What Just Happened?
Grove uses a technology called CRDTs (Conflict-free Replicated Data Types). The short version:
flowchart LR
subgraph "Traditional Sync"
T1[User A edits] --> TC{Conflict!}
T2[User B edits] --> TC
TC --> TL[Data loss or manual merge]
end
subgraph "Grove (CRDT)"
G1[User A edits] --> GM[Automatic Merge]
G2[User B edits] --> GM
GM --> GR[Both changes preserved]
endHow it works:
- Every change is an operation — "add node X", "update node Y"
- Operations have unique IDs — Based on replica + timestamp
- Merge is mathematical — Same inputs always produce same output
- Order doesn't matter — A+B = B+A (commutative)
This means:
- Users can edit offline, sync later
- Multiple users can edit simultaneously
- No central server needed for conflict resolution
- Data never gets lost or overwritten unexpectedly
Real-World Integration
In a real app, you'd use Phoenix PubSub or similar to broadcast operations:
# When local changes happen
{tree, ops} = Grove.Tree.flush_pending_ops(tree)
Phoenix.PubSub.broadcast(MyApp.PubSub, "doc:#{doc_id}", {:grove_ops, ops})
# When remote changes arrive
def handle_info({:grove_ops, ops}, socket) do
tree = apply_remote_ops(socket.assigns.tree, ops)
{:noreply, assign(socket, tree: tree)}
endGrove also provides Grove.Session for managing document sessions with multiple subscribers.
Next Steps
- Concepts — Understand how CRDTs work and why Grove is fast
- Use Cases — See real-world applications and patterns
- Architecture — Deep dive into Grove's internals
- Benchmarks — Performance characteristics
Quick Reference
| Operation | Code |
|---|---|
| Create tree | Grove.Tree.new("replica_id") |
| Add node | Grove.Tree.put_node(tree, node) |
| Get node | Grove.Tree.get_node(tree, id) |
| Update node | Grove.Tree.update_node(tree, id, fn) |
| Delete node | Grove.Tree.delete_node(tree, id) |
| Move node | Grove.Tree.move_node(tree, id, new_parent_id) |
| Get pending ops | Grove.Tree.flush_pending_ops(tree) |
| Apply remote op | Grove.Tree.apply_remote(tree, event) |