Use Cases
View SourceGrove excels in applications where data needs to sync across devices or users without conflicts. Here are real-world patterns you can implement.
Offline-First Applications
Apps that work without internet and sync when connected.
sequenceDiagram
participant U as User
participant A as App (Local)
participant S as Server
Note over U,S: User goes offline
U->>A: Create task
U->>A: Edit notes
U->>A: Complete item
Note over A: All changes stored locally
Note over U,S: User comes online
A->>S: Sync pending operations
S->>A: Receive remote changes
Note over A,S: Everything merged automaticallyExample: Offline Todo App
defmodule MyApp.OfflineTodos do
alias Grove.Tree
alias Grove.Node
def create_todo_list(user_id) do
# Each user gets their own replica ID
Tree.new("user_#{user_id}")
end
def add_task(tree, title) do
task_id = generate_id()
node = %Node{
id: task_id,
type: :task,
attrs: %{
title: title,
done: false,
created_at: DateTime.utc_now()
}
}
Tree.put_node(tree, node)
end
def complete_task(tree, task_id) do
Tree.update_node(tree, task_id, fn node ->
%{node | attrs: Map.put(node.attrs, :done, true)}
end)
end
# When connection restored, sync with server
def sync_with_server(tree, server_ops) do
# Apply remote operations
tree = Enum.reduce(server_ops, tree, fn {_id, event}, acc ->
case Tree.apply_remote(acc, event) do
{:ok, updated} -> updated
{:duplicate, existing} -> existing
end
end)
# Get local changes to send
{tree, local_ops} = Tree.flush_pending_ops(tree)
{tree, local_ops}
end
defp generate_id, do: :crypto.strong_rand_bytes(8) |> Base.encode16()
endWhy Grove?
- Changes work offline immediately
- No "pending" or "syncing" states to manage
- Conflicts resolve automatically when online
Multi-Device Sync
Same user, multiple devices, seamless sync.
flowchart LR
subgraph "Same User"
L[Laptop]
P[Phone]
T[Tablet]
end
S[(Sync Server)]
L <-->|ops| S
P <-->|ops| S
T <-->|ops| S
style S fill:#e1f5feExample: Notes App
defmodule MyApp.Notes do
alias Grove.Tree
alias Grove.Node
@doc """
Each device needs a unique replica ID within the same account.
Format: {account_id}_{device_id}
"""
def init_on_device(account_id, device_id) do
Tree.new("#{account_id}_#{device_id}")
end
def create_note(tree, title) do
note_id = generate_id()
note = %Node{
id: note_id,
type: :note,
attrs: %{title: title, content: "", updated_at: now()}
}
Tree.put_node(tree, note)
end
def update_content(tree, note_id, content) do
Tree.update_node(tree, note_id, fn node ->
%{node | attrs: node.attrs |> Map.put(:content, content) |> Map.put(:updated_at, now())}
end)
end
# Real-time sync via Phoenix PubSub
def broadcast_changes(tree, account_id) do
{tree, ops} = Tree.flush_pending_ops(tree)
if map_size(ops) > 0 do
Phoenix.PubSub.broadcast(
MyApp.PubSub,
"notes:#{account_id}",
{:grove_ops, ops}
)
end
tree
end
def handle_remote_ops(tree, ops) do
Enum.reduce(ops, tree, fn {_id, event}, acc ->
case Tree.apply_remote(acc, event) do
{:ok, updated} -> updated
{:duplicate, existing} -> existing
end
end)
end
defp now, do: DateTime.utc_now()
defp generate_id, do: :crypto.strong_rand_bytes(8) |> Base.encode16()
endWhy Grove?
- Edit on phone, see it on laptop instantly
- Both devices can edit simultaneously
- No "sync conflict" dialogs
Collaborative Editing
Multiple users editing the same document in real-time.
sequenceDiagram
participant A as Alice
participant S as Server
participant B as Bob
par Concurrent Edits
A->>S: Add "Meeting agenda"
B->>S: Add "Project notes"
end
S->>A: Bob's changes
S->>B: Alice's changes
Note over A,B: Both see: Meeting agenda + Project notesExample: Shared Document
defmodule MyApp.SharedDoc do
alias Grove.Tree
alias Grove.Node
@doc """
Create a new shared document.
Each participant joins with their own replica ID.
"""
def join_document(doc_id, user_id) do
Tree.new("#{doc_id}_#{user_id}")
end
def add_section(tree, title, opts \\ []) do
parent_id = Keyword.get(opts, :parent_id, "root")
section_id = generate_id()
section = %Node{
id: section_id,
type: :section,
parent_id: parent_id,
attrs: %{title: title, created_by: tree.replica_id}
}
Tree.put_node(tree, section)
end
def add_paragraph(tree, section_id, text) do
para_id = generate_id()
para = %Node{
id: para_id,
type: :paragraph,
parent_id: section_id,
attrs: %{text: text, author: tree.replica_id}
}
Tree.put_node(tree, para)
end
def move_section(tree, section_id, new_parent_id) do
# Grove prevents cycles automatically
Tree.move_node(tree, section_id, new_parent_id)
end
@doc """
Get document as nested structure for rendering.
"""
def render_document(tree) do
tree
|> Tree.children("root")
|> Enum.map(&render_node(tree, &1))
end
defp render_node(tree, node) do
children = tree |> Tree.children(node.id) |> Enum.map(&render_node(tree, &1))
Map.put(node, :children, children)
end
defp generate_id, do: :crypto.strong_rand_bytes(8) |> Base.encode16()
endWhy Grove?
- Real-time collaboration without operational transform complexity
- Automatic ordering of concurrent additions
- Safe tree restructuring (no cycles possible)
Local-First Software
Data lives on user's device, optionally syncs to cloud.
flowchart TB
subgraph "User's Device"
A[App] --> D[(Local Data)]
D --> A
end
D <-.->|Optional Sync| C[(Cloud Backup)]
style D fill:#c8e6c9
style C fill:#e1f5fe,stroke-dasharray: 5 5Example: Personal Knowledge Base
defmodule MyApp.KnowledgeBase do
alias Grove.Tree
alias Grove.Node
@doc """
Initialize knowledge base.
Data is stored locally, cloud sync is optional.
"""
def new(device_id) do
tree = Tree.new(device_id)
# Create root folders
tree
|> Tree.put_node(%Node{id: "inbox", type: :folder, attrs: %{name: "Inbox"}})
|> Tree.put_node(%Node{id: "archive", type: :folder, attrs: %{name: "Archive"}})
end
def add_note(tree, folder_id, title, content) do
note_id = generate_id()
note = %Node{
id: note_id,
type: :note,
parent_id: folder_id,
attrs: %{
title: title,
content: content,
tags: [],
created_at: DateTime.utc_now()
}
}
Tree.put_node(tree, note)
end
def add_tag(tree, note_id, tag) do
Tree.update_node(tree, note_id, fn node ->
tags = [tag | node.attrs[:tags] || []] |> Enum.uniq()
%{node | attrs: Map.put(node.attrs, :tags, tags)}
end)
end
def move_to_folder(tree, note_id, folder_id) do
Tree.move_node(tree, note_id, folder_id)
end
def search_by_tag(tree, tag) do
tree.nodes
|> Map.values()
|> Enum.filter(fn node ->
node.type == :note && tag in (node.attrs[:tags] || [])
end)
end
@doc """
Export all operations for backup/sync.
"""
def export_for_backup(tree) do
{tree, ops} = Tree.flush_pending_ops(tree)
{tree, :erlang.term_to_binary(ops)}
end
@doc """
Import operations from backup/sync.
"""
def import_from_backup(tree, binary) do
ops = :erlang.binary_to_term(binary)
Enum.reduce(ops, tree, fn {_id, event}, acc ->
case Tree.apply_remote(acc, event) do
{:ok, updated} -> updated
{:duplicate, existing} -> existing
end
end)
end
defp generate_id, do: :crypto.strong_rand_bytes(8) |> Base.encode16()
endWhy Grove?
- User owns their data (stored locally)
- Works without internet
- Optional sync preserves user privacy
Form Builders
Dynamic forms where fields can be added, reordered, and nested.
graph TD
F[Form] --> S1[Step 1: Personal]
F --> S2[Step 2: Address]
S1 --> N[Name Field]
S1 --> E[Email Field]
S2 --> A[Address Field]
S2 --> C[City Field]Example: Multi-Step Form Builder
defmodule MyApp.FormBuilder do
alias Grove.Tree
alias Grove.Node
def new_form(replica_id, title) do
tree = Tree.new(replica_id)
form = %Node{
id: "form_root",
type: :form,
attrs: %{title: title, version: 1}
}
Tree.put_node(tree, form)
end
def add_step(tree, title) do
step_id = generate_id()
step = %Node{
id: step_id,
type: :step,
parent_id: "form_root",
attrs: %{title: title, order: next_order(tree, "form_root")}
}
Tree.put_node(tree, step)
end
def add_field(tree, step_id, field_type, label, opts \\ []) do
field_id = generate_id()
field = %Node{
id: field_id,
type: :field,
parent_id: step_id,
attrs: %{
field_type: field_type,
label: label,
required: Keyword.get(opts, :required, false),
validation: Keyword.get(opts, :validation, nil),
order: next_order(tree, step_id)
}
}
Tree.put_node(tree, field)
end
def reorder_field(tree, field_id, new_order) do
Tree.update_node(tree, field_id, fn node ->
%{node | attrs: Map.put(node.attrs, :order, new_order)}
end)
end
def move_field_to_step(tree, field_id, new_step_id) do
Tree.move_node(tree, field_id, new_step_id)
end
def get_form_structure(tree) do
steps = tree
|> Tree.children("form_root")
|> Enum.sort_by(& &1.attrs[:order])
Enum.map(steps, fn step ->
fields = tree
|> Tree.children(step.id)
|> Enum.sort_by(& &1.attrs[:order])
%{step: step, fields: fields}
end)
end
defp next_order(tree, parent_id) do
tree
|> Tree.children(parent_id)
|> Enum.map(& &1.attrs[:order] || 0)
|> Enum.max(fn -> 0 end)
|> Kernel.+(1)
end
defp generate_id, do: :crypto.strong_rand_bytes(8) |> Base.encode16()
endWhy Grove?
- Multiple team members can edit forms simultaneously
- Drag-and-drop reordering syncs correctly
- Field moves between steps are atomic
When NOT to Use Grove
Grove isn't the right choice for every scenario:
| Scenario | Why Not Grove | Better Alternative |
|---|---|---|
| Financial transactions | Requires strong consistency, exact ordering | Database with ACID transactions |
| Inventory with limited stock | Last-write-wins can oversell | Centralized counter with locks |
| Authentication state | Security-critical, needs server authority | Server-authoritative sessions |
| Real-time game physics | Needs deterministic frame-by-frame sync | Lockstep or rollback netcode |
| Append-only logs | Grove is for trees, not sequences | Kafka, event sourcing |
Decision Guide
flowchart TD
Q1{Need offline support?} -->|Yes| Q2
Q1 -->|No| Q3
Q2{Conflicts acceptable to auto-resolve?} -->|Yes| G[Use Grove]
Q2 -->|No| T[Use transactions]
Q3{Multi-user real-time?} -->|Yes| Q4
Q3 -->|No| D[Use database directly]
Q4{Tree-structured data?} -->|Yes| G
Q4 -->|No| O[Consider other CRDTs]
style G fill:#c8e6c9Use Grove when:
- Users need to work offline
- Multiple devices/users edit the same data
- Data is naturally tree-structured
- "Both changes should be kept" is acceptable conflict resolution
Don't use Grove when:
- You need guaranteed ordering (e.g., financial ledger)
- Conflicts must be manually resolved
- Data requires server-side validation before accepting
- You need strong consistency guarantees
Next Steps
- Getting Started — Quick start tutorial
- Concepts — Understand how CRDTs work
- Architecture — Implementation deep-dive
- Benchmarks — Performance characteristics