← Back to list

Connect, Render, Sync: Building a Real-time Chat UI with Phoenix LiveView. Part 2

Hi everyone, I’m Ilham. Welcome back! 👋

Ilham Taufik · 2025-12-08 06:52 · 5 claps · 4.8 min read
#phoenix-framework #elixir #elixirlang #web-development
Open on Medium ↗
Wiki topics: 🌐 · Web Development

Connect, Render, Sync: Building a Real-time Chat UI with Phoenix LiveView. Part 2

Hi everyone, I’m Ilham. Welcome back! 👋

https://x.com/LundukeJournal/status/1997280920097923354/photo/1

https://x.com/LundukeJournal/status/1997280920097923354/photo/1

In Part 1, we handled the necessary setup: we got PostgreSQL running via Docker, initialized our Phoenix project, and ensured everything was error-free.

Now that the foundation is ready, let’s get to the code. Welcome to Part 2.

✔️ Before we proceed, make sure:

Checklist:

  • The Phoenix project is running Run:
mix phx.server
//And open http://localhost:4000
  • PostgreSQL is running via Docker
  • **mix ecto.create was successful**
  • No errors in the Phoenix terminal

If everything is ✔️, we can get started right away.

Explanation of warnings after mix phx.server

1️⃣ inotify-tools missing [error] inotify-tools is needed to run file_system

This is not a fatal error. It is only used for:

  • Phoenix Live-reload (auto-refreshing the browser when files change).
  • It does not affect the application logic at all.

If you want to remove the warning (on Arch/Linux):

sudo pacman -S inotify-tools

2️⃣ watchman: command not found Phoenix attempts to use watchman for file watching (optional). This is also optional.

If you want to install it:

sudo pacman -S watchman

3️⃣ Session cookie invalid / stale Plug.Session could not verify incoming session cookie.

This appears when an old session exists in your browser, but the new server has a different signing key.

  • Since we just created a new Phoenix app, the session key changed → this warning is normal.

🔥 STEP 1 — Activate Phoenix PubSub (Real-time without manual WebSockets!)

The Goal: ✔ Send real-time messages between clients. ✔ Use the built-in Phoenix PubSub. ✔ Similar to previous series, but easier and more scalable. ✔ No need to build manual WebSockets.

PubSub is usually installed automatically in Phoenix 1.7+, but let’s verify and use it.

✔ 1. Open lib/chat_web/application.ex

Add PubSub as a child to the supervisor:

children = [
  ChatWeb.Telemetry,
  ChatWeb.Repo,
  {DNSCluster, query: Application.get_env(:chat_web, :dns_cluster_query) || :ignore},
  {Phoenix.PubSub, name: ChatWeb.PubSub}, # <--- Ensure this line exists
  # Start a worker by calling: ChatWeb.Worker.start_link(arg)
  # {ChatWeb.Worker, arg},
  # Start to serve requests, typically the last entry
  ChatWeb.Endpoint,
  # ChatWeb.Chat.Presence (We will add this later)
]

Note: In new Phoenix templates, PubSub is usually already there, but we are double-checking.

🔥 STEP 2 — Create a Simple PubSub Channel (No WebSockets yet)

We won’t use full Phoenix Channels just yet to keep things simple. We will use pure PubSub.

Create a new file: lib/chat_web/chat/pubsub.ex

defmodule ChatWeb.Chat.ChatPubSub do

  @topic "room:general"

  @spec subscribe() :: :ok | {:error, {:already_registered, pid()}}
  def subscribe do
    Phoenix.PubSub.subscribe(ChatWeb.PubSub, @topic)
  end

  @spec broadcast_message(any(), any()) :: :ok | {:error, any()}
  def broadcast_message(user, message) do
    Phoenix.PubSub.broadcast(ChatWeb.PubSub, @topic, {
      :new_msg, user, message
    })
  end

end

Super simple:

  • subscribe → joins the room (topic).
  • broadcast_message → sends a message to all subscribers.

🔥 STEP 3 — Create the LiveView Chat (Real-time UI)

Now, let’s build a simple User Interface.

Create the LiveView file: lib/chat_web_web/live/chat_live.ex

defmodule ChatWebWeb.ChatLive do
  use ChatWebWeb, :live_view

  # Mount is called when the client connects
  def mount(_params, _session, socket) do
    if connected?(socket) do
      # Subscribe to the PubSub topic
      ChatWeb.Chat.ChatPubSub.subscribe()

      # Track presence (who is online)
      ChatWeb.Chat.Presence.track(self(), "room:general", socket.id, %{
        joined_at: DateTime.utc_now
      })
    end

    # Get initial list of online users
    presences = ChatWeb.Chat.Presence.list("room:general")

    {:ok, socket
    |> assign(:messages, [])
    |> assign(:input, "")
    |> assign(:users, Map.keys(presences))}
  end

  # Handle "send" event from the form
  def handle_event("send", %{"msg" => msg}, socket) do
    ChatWeb.Chat.ChatPubSub.broadcast_message("guest", msg)
    {:noreply, assign(socket, :input, "")}
  end

  # Handle incoming broadcast messages
  def handle_info({:new_msg, user, msg}, socket) do
    new_list = socket.assigns.messages ++ ["#{user}: #{msg}"]
    {:noreply, assign(socket, :messages, new_list)}
  end

  # Handle presence changes (User Join/Leave)
  def handle_info(%{event: "presence_diff"}, socket) do
    presences = ChatWeb.Chat.Presence.list("room:general")
    {:noreply, assign(socket, :users, Map.keys(presences))}
  end
end

🔥 STEP 4 — Add Routing

We need to expose this LiveView to the browser.

Open: lib/chat_web_web/router.ex

Add the route inside the scope "/dev" block:

scope "/dev" do
  pipe_through :browser

  live_dashboard "/dashboard", metrics: ChatWebWeb.Telemetry
  forward "/mailbox", Plug.Swoosh.MailboxPreview

  # Add this line:
  live "/chat", ChatWebWeb.ChatLive
end

🔥 STEP 5 — Create the View (HEEX)

We need an HTML template to render the chat.

Create the file: lib/chat_web_web/live/chat_live.html.heex

<div class="max-w-xl mx-auto mt-10 space-y-4">

  <div class="mb-4">
    <h3 class="font-semibold mb-1">Online Users:</h3>
    <ul class="text-green-400">
      <%= for user <- @users do %>
        <li><%= user %></li>
      <% end %>
    </ul>
  </div>

  <h2 class="text-2xl font-bold mb-4">Chat</h2>

  <ul id="messages" class="bg-gray-800 p-4 rounded h-64 overflow-y-auto space-y-1">
    <%= for msg <- @messages do %>
      <li class="text-gray-200"><%= msg %></li>
    <% end %>
  </ul>

  <form phx-submit="send" class="flex space-x-2">
    <input type="text"
           name="msg"
           value={@input}
           autocomplete="off"
           placeholder="Type your message..."
           class="flex-1 px-3 py-2 bg-gray-900 border border-gray-600 text-white rounded"/>

    <button class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
      Send
    </button>
  </form>

</div>

🔥 STEP 6 — Run the Application

Time to test it!

mix phx.server

Open your browser to: 👉 **http://localhost:4000/dev/chat**

Now, open a second tab (or a different browser window) to the same URL. Type a message in one tab, and watch it appear instantly in the other. You will also see the “Online Users” list update automatically.

The Magic:

  • 🚫 No manual WebSockets code.
  • 🚫 No complex JavaScript framework.
  • ✅ Phoenix handles everything natively.

💡 Author’s Note on Routing: You might notice I placed the route inside the /dev scope (http://localhost:4000/dev/chat). I tried moving it to the main scope (/chat), but I kept hitting some tricky errors (likely related to CSRF or pipeline configuration). For the sake of this tutorial, running it under /dev works flawlessly. Feel free to tinker with the router if you want to solve that puzzle! 😃

Awesome! ✅

If the chat is rendering, messages are flying, and the UI is responsive — then Part 2 (PubSub Broadcast) is officially complete.

But right now, we are missing one crucial thing: Identity. We don’t know who is actually in the room.

Next Stop: Part 3 — Phoenix Presence 🕵️

In the next part, we will implement the “Who’s Online” feature using Phoenix Presence. The goal? ✅ Display exactly who is online in the chat room. ✅ Handle auto-updates when users join or leave. ✅ Real-time synchronization (zero page refreshes). ✅ No Database required — everything is stored in the BEAM cluster.

💎 The Real Treasure: Remember the distributed cluster we built in *[Persist, Cache, Distribute: An Elixir Chat Cluster Tutorial]*? Phoenix Presence is built on that exact same technology. In the next part, we won’t just track users on one computer; we are preparing to track them across the entire distributed node system.

Ready to give your users an identity? See you in Part 3! 🚀


메타데이터
post_id
d21d2e5d95ec
slug
connect-render-sync-building-a-real-time-chat-ui-with-phoenix-liveview-part-2-d21d2e5d95ec
url
https://medium.com/@ilhamtaufikp/connect-render-sync-building-a-real-time-chat-ui-with-phoenix-liveview-part-2-d21d2e5d95ec
canonical_url
https://medium.com/@ilhamtaufikp/connect-render-sync-building-a-real-time-chat-ui-with-phoenix-liveview-part-2-d21d2e5d95ec
author_url
https://medium.com/@ilhamtaufikp
status
ok
fetched_at
2026-06-22 12:55:45