Use Cases

View Source

Grove 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 automatically

Example: 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()
end

Why 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:#e1f5fe

Example: 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()
end

Why 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 notes

Example: 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()
end

Why 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 5

Example: 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()
end

Why 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()
end

Why 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:

ScenarioWhy Not GroveBetter Alternative
Financial transactionsRequires strong consistency, exact orderingDatabase with ACID transactions
Inventory with limited stockLast-write-wins can oversellCentralized counter with locks
Authentication stateSecurity-critical, needs server authorityServer-authoritative sessions
Real-time game physicsNeeds deterministic frame-by-frame syncLockstep or rollback netcode
Append-only logsGrove is for trees, not sequencesKafka, 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:#c8e6c9

Use 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