Elixir Functions

 

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.

Building a Chat Application with 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!

Previously at
Flag Argentina
Brazil
time icon
GMT-3
Tech Lead in Elixir with 3 years' experience. Passionate about Elixir/Phoenix and React Native. Full Stack Engineer, Event Organizer, Systems Analyst, Mobile Developer.