Connect, Render, Sync: Building a Real-time Chat UI with Phoenix LiveView. Part 2
Hi everyone, I’m Ilham. Welcome back! 👋
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
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.createwas 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
/devscope (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/devworks 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