DEV Community

Cover image for How to Build Collaborative Real-Time Interfaces in Phoenix LiveView with CRDTs
HexShift
HexShift

Posted on • Edited on

How to Build Collaborative Real-Time Interfaces in Phoenix LiveView with CRDTs

Most collaborative apps break down under pressure:

Edits vanish. Cursors clash. Users step on each other’s work.

But with Phoenix LiveView and CRDTs, you can build conflict-free, multi-user interfaces that feel as smooth as Google Docs — all from the server.


The Challenge: True Collaboration

If two users type at once, what happens?

With naïve state management, one user “wins” and the other’s edit gets dropped or overwritten.

To solve this, you need:

  • ⚡ Instant updates over WebSockets
  • 🔀 Mergeable data structures
  • 🖥️ Shared presence, cursors, highlights
  • 🧠 State that survives latency and disconnects

LiveView gives you the transport and rendering.

CRDTs give you the consistency.


Step 1: Broadcast User Actions

Each edit is a discrete operation, not a blob of text.

Phoenix.PubSub.broadcast(MyApp.PubSub, "doc:123", %{
  op: :insert,
  pos: 24,
  char: "a",
  user_id: "u9"
})
Enter fullscreen mode Exit fullscreen mode

In your LiveView:

def mount(%{"doc_id" => doc_id}, _session, socket) do
  Phoenix.PubSub.subscribe(MyApp.PubSub, "doc:#{doc_id}")
  {:ok, assign(socket, crdt: MyCRDT.new())}
end

def handle_info(%{op: _, pos: _, char: _, user_id: _} = change, socket) do
  updated_crdt = MyCRDT.apply(socket.assigns.crdt, change)
  {:noreply, assign(socket, crdt: updated_crdt)}
end
Enter fullscreen mode Exit fullscreen mode

Step 2: Use a Mergeable CRDT

Use a sequence-based CRDT (like RGA or Yjs) to track insertions and deletions.

You can use:

  • delta_crdt_ex for peer sync
  • A custom GenServer that holds the CRDT and rebroadcasts deltas
  • An in-memory copy per LiveView for performance

Each client maintains their own copy. Changes are applied in the same order everywhere — no locks, no overwrites.


Step 3: Render the Shared State

<div class="prose whitespace-pre-wrap font-mono">
  <%= for char <- MyCRDT.to_list(@crdt) do %>
    <%= char %>
  <% end %>
</div>
Enter fullscreen mode Exit fullscreen mode

LiveView only re-renders the changed text, thanks to smart DOM patching.

Result?

Millisecond updates. Zero flicker. Scales with document size.


Step 4: Show Live Cursors + Presence

Use Phoenix.Presence to track active users:

presence_list = Phoenix.Presence.list(MyApp.Presence, "doc:123")

# Example result:
%{
  "u9" => %{metas: [%{cursor: {2, 18}, name: "Alice"}]},
  "u5" => %{metas: [%{cursor: {7, 43}, name: "Bob"}]}
}
Enter fullscreen mode Exit fullscreen mode

In the template:

<%= for {user_id, %{metas: [meta | _]}} <- @presence do %>
  <div
    class="absolute w-2 h-4 bg-blue-300 rounded-sm transition-all"
    style={"left: #{meta.cursor.x}px; top: #{meta.cursor.y}px;"}
    title={meta.name}
  ></div>
<% end %>
Enter fullscreen mode Exit fullscreen mode

Each user gets a cursor. You can animate them, style them, or show tooltips.


Step 5: Persist & Replay

Batch operations into an event log:

Repo.insert!(%ChangeEvent{doc_id: "123", payload: change})
Enter fullscreen mode Exit fullscreen mode

On reconnect:

  • Replay events
  • Rebuild the CRDT
  • Re-sync local state

CRDTs guarantee that merge order doesn’t matter. You always converge.


Extend to Whiteboards, Diagrams, Tables

Text is just the start.

You can make each visual element a CRDT item:

%{
  id: "shape-42",
  type: :circle,
  x: 200,
  y: 150,
  radius: 40
}
Enter fullscreen mode Exit fullscreen mode

On drag:

Phoenix.PubSub.broadcast(MyApp.PubSub, "whiteboard:room1", %{
  op: :move,
  id: "shape-42",
  x: 260,
  y: 180,
  user_id: "u3"
})
Enter fullscreen mode Exit fullscreen mode

Each change is mergeable. Each canvas stays consistent.

Use Tailwind (absolute, transform, transition, etc.) to animate it all without JS.


Why This Works

✅ CRDTs make your state conflict-free

✅ LiveView makes your UI real-time

✅ PubSub + Presence makes it collaborative

✅ One stack — all Elixir — no frontend framework needed

You don’t need React. You don’t need Firebase.

You need Phoenix + CRDTs + good architecture.


Ready to Build Collaborative Tools That Scale?

Download the PDF:

Phoenix LiveView: The Pro’s Guide to Scalable Interfaces and UI Patterns

Inside you’ll find:

  • Shared editing patterns
  • CRDT data models
  • Real-time cursor UX
  • Tailwind-driven styling
  • Event log persistence
  • Presence-based UI design

Whether you’re building a team notes app, a live whiteboard, or a multiplayer canvas — you can make it fast, safe, and seamless with LiveView.

No hacks. No JS sprawl. Just Elixir, done right.

Top comments (0)

OSZAR »