Build a small chat service using Elixir and deploy it on Amazon ec2 using AWS (Part 1)
Author: Tam Ly
Published: 2024-09-14

Introduction

Let’s build a distributed chat service application using Elixir. It will be a simple project where users can join rooms and send messages to others, but it will also introduce some fascinating concepts and foundational building blocks in Elixir development.

This application will leverage key techniques such as LiveView, LiveComponent, Channels, PubSub, and Ecto. We’ll deploy it on AWS EC2 instances, and the application will be compiled locally using Docker.

This blog series will cover:

This is not an exhaustive guide and won’t cover every use case. It may not adhere to all security best practices, as the goal is to explore interesting concepts and provide a starting point for your development journey. Additionally, the application may not handle all error cases and could report errors unexpectedly.

Interested? Let’s get started!

Ecto Introduction

Ecto is split into 4 main components:

Ecto.Repo - repositories are wrappers around the data store. Via the repository, we can create, update, destroy and query existing entries. A repository needs an adapter and credentials to communicate to the database

Ecto.Schema - schemas are used to map external data into Elixir structs. We often use them to map database tables to Elixir data but they have many other use cases

Ecto.Query - written in Elixir syntax, queries are used to retrieve information from a given repository. Ecto queries are secure and composable

Ecto.Changeset - changesets provide a way to track and validate changes before they are applied to the data

In summary:

Ecto.Repo - where the data is Ecto.Schema - what the data is Ecto.Query - how to read the data Ecto.Changeset - how to change the data

Implement Database

The database of the application will be simple including 4 tables: Chat, User, Room and Member.

Overview of the table as bellow diagram: database_chat_room

Besides, we will use sqlite3 for communicating to the database instead of Postgres because when using sqlite3 we do not need to set up user name and password for it, and it is suitable for this small chat application.

We will name the application ChatService and create a room directory under lib/chat_service. In this directory, we will define the schemas and changesets for four tables within lib/chat_service/room, such as: chat, member_info, room_info and user_info.

We will take a look at user_info.ex and room_info.ex:

room_info.ex

defmodule ChatService.Room.RoomInfo do
  use Ecto.Schema
  import Ecto.Changeset

  @primary_key {:room_id, :id, autogenerate: true}
  schema "room" do
    field :channel_id, :string
    has_many :chat, ChatService.Room.ChatInfo, foreign_key: :room_id, references: :room_id
    has_many :member, ChatService.Room.MemberInfo, foreign_key: :room_id, references: :room_id

    timestamps()

  end

  def changeset(room, params \\ %{}) do
    room
    |> cast(params, [:channel_id])
    |> validate_required([:channel_id])
    |> unique_constraint(:room_id)
  end

end

user_info.ex:

defmodule ChatService.Room.UserInfo do
  use Ecto.Schema
  import Ecto.Changeset

  @primary_key {:user_id, :string, autogenerate: false}
  schema "user" do
    has_many :chat, ChatService.Room.ChatInfo, foreign_key: :user_id, references: :user_id
    has_many :member, ChatService.Room.MemberInfo, foreign_key: :user_id, references: :user_id
    timestamps()

  end

  def changeset(chat, params \\ %{}) do
    chat
    |> cast(params, [:user_id])
    |> unique_constraint(:user_id)
    |> validate_required([:user_id])
  end

  def registration_changeset(user, params \\ %{}) do
    cast(user, params, [])
  end
end

As you can see, we will use @primary_key in the user and room schemas to specifically configure the schema’s primary key and to indicate a one-to-many association with another schema we will use has_many.

After creating all schemas and changesets, we will generate a migration using the command mix ecto.gen.migration create_room_table. Once generated, you can check the priv/repo/migrations directory, where you’ll find a new migration file with a timestamp like <time_stamp>_create_room_table.ex.

In this migration file, we will create four tables, and we’ll also add unique indexes for the user, room, and member tables.

To migrate a repository we will use command mix ecto.migrate.

API interaction with database

We will create a set of APIs to interact with the database in the lib/chat_service/room directory, and we’ll name this module room_action.ex, such as: add_room, add_chat, add_user, etc.

defmodule ChatService.Room.RoomActions do

  alias ChatService.Room.ChatInfo
  alias ChatService.Room.RoomInfo
  alias ChatService.Room.UserInfo
  alias ChatService.Room.MemberInfo
  alias ChatService.Repo
  import Ecto.Query

  @doc """
  Add RoomInfo entry with channel id.
  """
  @spec add_room(map()) ::
    {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
  def add_room(attrs \\ %{}) do
    %RoomInfo{}
    |> RoomInfo.changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Add ChatInfo entry.
  """
  @spec add_chat(map()) ::
    {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
  def add_chat(attrs \\ %{}) do
    %ChatInfo{}
    |> ChatInfo.changeset(attrs, [:user_id, :room_id])
    |> Repo.insert()
  end

  ...
end

Next part, we will discover more technique to build the application such as: LiveView, LiveComponent, PubSub, and Channels. (Part 2)

Repo: https://github.com/ohhi-vn/embedded_chat