Building a Chat Application with Elixir and Phoenix Channels
In the world of real-time applications, building a chat application that can handle multiple users and deliver messages instantly is a common requirement. Elixir, a functional and concurrent programming language, along with the Phoenix web framework, can be a powerful combination to create such applications. In this blog post, we will guide you through the process of building a chat application from scratch using Elixir and Phoenix Channels.
Prerequisites
Before diving into the project, you should have a basic understanding of Elixir and Phoenix. If you’re new to these technologies, it’s recommended to go through some beginner-level tutorials to familiarize yourself with the concepts. Additionally, ensure that you have Elixir and Phoenix installed on your machine.
Setting Up the Project
Let’s start by setting up the basic structure of our chat application.
Step 1: Install Phoenix
If you haven’t installed Phoenix yet, you can do so by following the official installation guide at Phoenix Installation Guide.
Step 2: Create a New Phoenix Project
Open your terminal and run the following command to create a new Phoenix project:
bash mix phx.new chat_app
Step 3: Configure the Database
Navigate to the newly created project folder:
bash cd chat_app
Next, create and migrate the database:
bash mix ecto.create mix ecto.migrate
Step 4: Start the Phoenix Server
Start the Phoenix development server with the following command:
bash mix phx.server
Now, you can visit http://localhost:4000 in your web browser to see the default Phoenix welcome page.
Implementing Authentication
To allow users to join the chat and interact, we need to implement user authentication. We will use Guardian, an authentication library for Elixir, to handle user registration and login.
Step 1: Add Guardian to Dependencies
Open the mix.exs file and add :guardian and :argon2_elixir to the list of dependencies:
elixir defp deps do [ # other dependencies... {:guardian, "~> 2.0"}, {:argon2_elixir, "~> 2.0"} ] end
Step 2: Install and Compile Dependencies
Run the following command to fetch and compile the new dependencies:
bash mix deps.get mix deps.compile
Step 3: Generate Guardian Secret Key
Generate a new Guardian secret key using the following command:
bash mix guardian.gen.secret
Copy the generated key and add it to your config/dev.exs and config/test.exs files:
elixir config :guardian, Guardian, allowed_algos: ["HS512"], secret_key: "YOUR_GENERATED_SECRET_KEY"
Step 4: Implement User Authentication
Create a new file named user.ex under the lib/chat_app directory and define the user schema and changeset:
elixir defmodule ChatApp.User do use Ecto.Schema import Ecto.Changeset schema "users" do field :username, :string field :email, :string field :password, :string, virtual: true field :password_hash, :string timestamps() end def changeset(user, attrs) do user |> cast(attrs, [:username, :email, :password]) |> validate_required([:username, :email, :password]) |> unique_constraint(:email) |> put_password_hash() end defp put_password_hash(changeset) do case changeset.valid? do true -> case changeset.data[:password] do nil -> changeset password -> put_change(changeset, :password_hash, Comeonin.Argon2.hashpwd_salt(password)) end false -> changeset end end end
Next, create a new migration to add the users table to the database:
bash mix ecto.gen.migration create_users
Open the generated migration file and modify it as follows:
elixir defmodule ChatApp.Repo.Migrations.CreateUsers do use Ecto.Migration def change do create table(:users) do add :username, :string add :email, :string add :password_hash, :string timestamps() end create unique_index(:users, [:email]) end end
Now, apply the migration to create the users table:
bash mix ecto.migrate
Step 5: User Registration and Login
Let’s implement user registration and login functionality in the chat application. Add the following code snippets to the corresponding files:
web/router.ex
elixir defmodule ChatAppWeb.Router do use ChatAppWeb, :router # other pipelines... pipeline :browser_session do plug Guardian.Plug.VerifySession, claims: %{"typ" => "access"} plug Guardian.Plug.LoadResource end scope "/", ChatAppWeb do pipe_through :browser # other routes... get "/register", UserController, :new post "/register", UserController, :create get "/login", SessionController, :new post "/login", SessionController, :create delete "/logout", SessionController, :delete end end
web/controllers/user_controller.ex
elixir defmodule ChatAppWeb.UserController do use ChatAppWeb, :controller def new(conn, _params) do render(conn, "new.html") end def create(conn, %{"user" => user_params}) do changeset = User.changeset(%User{}, user_params) case Repo.insert(changeset) do {:ok, _user} -> conn |> Guardian.Plug.sign_in(changeset.data) |> redirect(to: "/") {:error, changeset} -> render(conn, "new.html", changeset: changeset) end end end
web/controllers/session_controller.ex
elixir defmodule ChatAppWeb.SessionController do use ChatAppWeb, :controller def new(conn, _params) do render(conn, "new.html") end def create(conn, %{"session" => session_params}) do user = Repo.get_by(ChatApp.User, email: session_params["email"]) case Guardian.authenticate(user, session_params["password"]) do {:ok, user, _claims} -> conn |> Guardian.Plug.sign_in(user) |> redirect(to: "/") {:error, _reason} -> conn |> put_flash(:error, "Invalid email or password") |> redirect(to: "/login") end end def delete(conn, _params) do conn |> Guardian.Plug.sign_out() |> redirect(to: "/") end end
Step 6: Creating a Chat Room
Now that we have user authentication in place, let’s move on to creating a chat room using Phoenix Channels.
Step 1: Generate a Channel
Create a new Phoenix Channel by running the following command:
bash mix phx.gen.channel ChatRoom
This will generate a new channel, along with a test file, migration, and other necessary files.
Step 2: Update the Socket
In lib/chat_app_web/channels/user_socket.ex, add the newly generated chat room channel to the list of allowed channels:
elixir defmodule ChatAppWeb.UserSocket do use Phoenix.Socket # other code... channel "chat_room:*", ChatAppWeb.ChatRoomChannel end
Step 3: Implement the Chat Room Channel
Edit the file lib/chat_app_web/channels/chat_room_channel.ex and update it as follows:
elixir defmodule ChatAppWeb.ChatRoomChannel do use Phoenix.Channel def join("chat_room:" <> room_id, _payload, socket) do {:ok, socket} end def handle_in("new_message", %{"content" => content}, socket) do broadcast(socket, "new_message", %{ "username" => socket.assigns.current_user.username, "content" => content }) {:noreply, socket} end end
Step 4: Create the Chat Room Page
Create a new file named chat_room_live.ex in the lib/chat_app_web/live directory with the following content:
elixir defmodule ChatAppWeb.ChatRoomLive do use Phoenix.LiveView def mount(_params, _session, socket) do {:ok, assign(socket, current_user: socket.assigns.current_user)} end def render(assigns) do ~L""" <div> <h1>Welcome to the Chat Room, <%= @current_user.username %></h1> <div id="chat-box"> <%= for message <- @messages do %> <p><strong><%= message["username"] %>:</strong> <%= message["content"] %></p> <% end %> </div> <form phx-submit="new_message" phx-change="disableButton"> <input type="text" name="content" placeholder="Type your message here..." /> <button id="send-button" disabled="disabled">Send</button> </form> </div> """ end def handle_event("new_message", %{"content" => content}, socket) do ChatAppWeb.Endpoint.broadcast("chat_room:lobby", "new_message", %{ "username" => socket.assigns.current_user.username, "content" => content }) {:noreply, assign(socket, messages: [message | socket.assigns.messages])} end defp message(%{"username" => username, "content" => content}) do %{"username" => username, "content" => content} end end
Step 5: Update the Router
In the web/router.ex file, add the chat room route:
elixir defmodule ChatAppWeb.Router do use ChatAppWeb, :router # other code... scope "/", ChatAppWeb do pipe_through :browser get "/", PageController, :index live "/chat", ChatRoomLive end end
Conclusion
Congratulations! You’ve successfully built a real-time chat application using Elixir and Phoenix Channels. We covered the basics of user authentication and implemented a chat room using Phoenix Channels and LiveView. Elixir’s concurrent and fault-tolerant nature makes it an excellent choice for building scalable and responsive real-time applications.
Remember, this is just a starting point, and there’s a lot more you can do to enhance your chat application. You can add features like private messaging, user presence tracking, or even group chat rooms. Happy coding!
Remember that this blog is a basic guide to building a chat application. There’s much more you can explore and improve upon, including optimizing performance, adding more features, and handling edge cases. Happy coding!
Table of Contents