Build Real-time Chat With Phoenix and LiveView in Fewer Than 50 Lines of Code

If you haven’t experienced the awesome power of LiveView in the Phoenix framework, strap in: we’re going to build a real-time, high-performance chat system with fewer than 50 lines of code. That includes all the code for both the frontend and the backend (spoiler alert: they’re kind of the same thing).

Our basic strategy in this project will be to use a persistent Phoenix LiveView process for each user who connects to the server, and then use Phoenix’s built-in PubSub (“publish/subscribe”) functionality to broadcast messages to all of the connected processes. Finally, the LiveView processes will render the messages back to each connected user.

To keep this example as minimal as possible, we’re going to cut some corners to focus on the core use case. Some specific things we won’t address (but are simple additions for those who want to learn more) are user authentication and management; chat message persistence; and support for multiple channels with a lobby.

Prerequisites — What is Phoenix? What is LiveView?

Before starting on this project, you’ll need to follow the instructions to install both Elixir and Phoenix.

Briefly, Phoenix is a sophisticated web framework written in the amazing Elixir language. Elixir is so amazing in part because it’s built on the Erlang Virtual Machine, which means it supports highly available and highly concurrent applications by nature.

The Phoenix web framework takes advantage of these features and enables LiveView for highly interactive server-rendered content. That means you can build highly interactive “single page”-style webapps without writing a line of JavaScript. I’ve written previously about LiveView and why I think it’s such an amazing tool.

If you haven’t worked with this stack before, this is the perfect opportunity to try it out and see the power for yourself. Let’s get started!

Step 1 — Project Setup

Once you’ve got Elixir & Phoenix installed, the first thing we’re going to do is create the project. Assuming your environment is properly configured, you can generate a full Phoenix project directory with:mix phx.new liveview_chat --no-ecto

The --no-ecto flag here means that we don’t want to include the Ecto datamapper library in our project. We’re doing so in this example for simplicity, and so that we don’t need to worry about setting up a database. If you’re planning to build this demo project into something bigger, you may wish to include Ecto functionality by omitting the --no-ecto flag.

Follow the steps from mix to get your project created and the dependencies installed. If everything goes right, you should be able to run your Phoenix server with mix phx.server and view the default homepage by navigating to http://localhost:4000 in your browser.

Next, we’re going to create a route (/chat) for our new chat page by editing the router file at lib/liveview_chat_web/router.ex . This is the path users will navigate to in order to access the chat. We’ll also tell it which module will render our LiveView. In this case that module will be named LiveviewChatWeb.ChatLive.Index. To set up the route, add thelive line in the block shown below:

Step 2 — LiveView Skeleton

Next, we’re going to set up our LiveView. To keep this simple and move step by step, we’ll start with a minimal version of both the LiveView module, and the HTML template. Then, in our subsequent steps, we’ll fill in the functionality.

First, let’s create a simple HTML template for the page. Because we’re going for simplicity here, we’re using some minimal inline styling — in a real app, we’d want to separate the styling out or use a framework like Tailwind.

Here’s what our basic template looks like:

The HTML should be pretty self-explanatory and look like any other HTML you might write, with one exception: the attribute phx-submit=”send” on our form. That attribute is a LiveView binding, which says we want deliver a message to our backend module when the form is submitted.

If you’ve done web development in the past, you’ll note that a form submission is a client-side event, but the LiveView module is running in a server-side process. LiveView and Phoenix hide this complexity from us, so we won’t need to write an API or a line of JavaScript to get this working. We’ll see how the event is consumed in the next step.

Now over to the backend, let’s write our skeletal LiveView module, just enough to get our HTML template to render:

The only method we need to write here is mount — it gets called when a user navigates to our /chat route and is our entry-point for the chat functionality. We get passed in request parameters and session information, but for our current example, we can ignore those. We also get passed in the socket, which represents the LiveView connection.

Upon success, LiveView expects us to return a tuple of {:ok, socket} (a common Elixir pattern for a successful function call). But before returning the socket, we also assign it some values. These assigns are persistent application state for the user who is connected to the LiveView. In this case, our application needs to store a username, and a list of messages. For demo purposes, we’ll use a random username — in a real app, you’ll probably want some actual user management!

If you’ve successfully added these files, you should be able to navigate to http://localhost:4000/chat and see the basic page. You won’t be able to send any messages yet though — we haven’t added that functionality yet!

Step 3 — Accept Chat Messages, and Broadcast

Next, let’s add a handler for the “send” event we’ll trigger on the frontend. We’ll do so with a handle_event function:

To handle those send events from the frontend, we implement a function with a declaration that looks like this:

def handle_event(“send”, %{“text” => text}, socket) do   
  ...
end

In case you’re not familiar with Elixir’s pattern matching, the first and second arguments here are not simple variables as they might be in other languages, but instead, are patterns to be matched. That means this function will only be invoked if the first parameter is “send” and the second is a dictionary containing a “text” key. If we add a new event from the client, we can create an entirely different function definition to handle that case, instead of having a single handle_event function with conditional logic.

In our above implementation, when we receive a “send” message from the client, we will publish a message via PubSub. The endpoint module that was generated for us supports PubSub by default. We just need to specify a topic, a message, and a payload. All subscribers of the topic will receive the message and payload. We’ll see how to handle that in the next step.

Our payload here is simple: the message that was sent, and the name of the person that sent it. In a real application, we’d have a more sophisticated data structure, but this is all we need for the demo.

Step 4 — Subscribe & Consume Messages

Next, having added our chat message broadcast, we need something to consume them! Let’s add the handle_info function below to complete our LiveView module.

In this update, we’ve added two things: first, in our mount function, we’ve added some lines to subscribe to the topic. Then, we’ve added a handle_infofunction to consume the messages that we’ve subscribed to.

You’ll notice that before we subscribe, we check connected?(socket)why? If you start building much with LiveView, you’ll sooner or later notice that mountgets called twice, which can cause some confusion and bugs if you’re not expecting it. The reason is that when a LiveView page is viewed, it’s actually a two step process: first, an HTTP GET request is made to get the initial content, and then afterwards, a Websocket connection is established to perform any interactive rendering. Any code to support interactions or persistent connections should only be executed in the “socket connected” case. See my previous post on LiveView for more details on why this two-step process is so important.

The other change here is the handle_info function. Like the handle_eventfunction we wrote in the previous step, we use pattern matching here to only process the events we care about. In this case, we update our “message” array in the socket’s assigns to add the new message coming through.

(Side note: in case you’re wondering, handle_info and handle_event are standard callback interfaces for the LiveView module — see the documentation if you’re curious about the details).

The only thing left to do is to update our HTML template to render the messages. We’ll update the “messages” div as shown below:

Step 5 — Chat!

With everything in place, you should be able to navigate to http://localhost:4000/chat in your browser and see the app in action. Open up the page in a separate window or browser, and send some chat messages to see the interaction between the two (or three, or four).

Next Steps

If you’ve made it this far, you’re done with the demo project in this post! But if you want to keep going, here are some simple things you can add to expand on this project:

  • support multiple channels via chat:channel_name
  • let users specify their nicknames and add basic user management
  • add a message persistence layer so new users can see chat history