Building an Anonymous Messaging App With React and Phoenix

Introduction
In this in-depth tutorial, we’ll build an anonymous messaging application. We’ll use React to create a dynamic user interface and Phoenix to manage the server-side logic.
What is an Anonymous Messaging App?
An anonymous messaging app allows users to receive messages without exposing the sender’s identity. These apps provide a space for candid feedback, confessions, or simply fun interactions. Popular examples of such services include Kubool (https://gdpd.xyz/) and NGL (https://ngl.link/).
Prerequisites
Before starting, please ensure we have the following:
- Elixir, Phoenix, and PostgreSQL: Follow the installation guide on the Phoenix documentation: https://hexdocs.pm/phoenix/installation.html
- Node.js and npm: If not installed, download Node.js from https://nodejs.org/en/download (this also installs
npm
).
Our application will blend two technologies, so basic knowledge of both is required:
- React: A widely used JavaScript library renowned for crafting modular and interactive user interface components.
- Phoenix: An Elixir-based framework acclaimed for its real-time communication capabilities and efficient handling of large volumes of concurrent connections.
Unlike traditional setups where a frontend interacts with a backend through API endpoints, we’ll seamlessly embed our React components within the Phoenix codebase.
App Requirements and Flow
Our anonymous messaging app will have a simple but interactive flow:
- Home Page: Introduces the app, displaying the “register” buttons.
- Registration: Users create accounts with a unique email address and password.
- User Page: Logged-in users see received messages (or a “no messages yet” notice). A shareable link will be displayed to the users that can be shared with others to send them anonymous messages.
- Message Sending: Anonymous users visiting a shareable link can type messages in a text field and send the message.
Application Routes
For our application, we’re focused on the following routes:
/
– Home page/users/register
– Registration page/users/log_in
– Login page/users/messages
– User’s messages page/send_messages/:recipient_id
– Page for sending messages anonymously to a specific user
Development Steps
Creating a New Phoenix Project
Assuming we have the prerequisites installed (refer to the Introduction), let’s open our terminal and create a new Phoenix project by running:
mix phx.new anon_messages
When prompted to fetch and install dependencies, type Y
and click enter.
When this is done, navigate to anon_messages
directory and create the database:
cd anon_messages && mix ecto.create
Finally, we start the application by running:
mix phx.server
Now we can view our application at http://localhost:4000.
Understanding the Routes (router.ex
)
In our generated project, the routes live within the lib/anon_messages_web/router.ex
file. In this file, we’ll see a line similar to this:
get "/", PageController, :home
This line maps the root path (/
) to the PageController
module and the home
action. When a user visits the homepage, the home
function in the PageController
gets executed, rendering the associated view. We’ll find the PageController
in lib/anon_messages_web/controllers/page_controller.ex
:
The PageController
renders views located in the lib/anon_messages_web/templates/page_html
directory. The home
action which we specified in the routes file specifically renders the home.html.heex
template. This HEEx file allows us to embed Elixir expressions within our HTML code.
Customizing the Landing Page
The default Phoenix setup provides a basic landing page. We’ll adapt this page by modifying the view to introduce our anonymous messaging concept. We’ll edit the lib/anon_messages_web/controllers/page_html/home.html.heex
template file. Let’s replace the content of this file with this:
When we refresh the app, the landing page should look like this:

Setting up the Authentication Flow
In this section, we’ll add an authentication layer to handle user registration and login securely, and we’ll create the user interface for the login and sign-up pages.
Generating an Authentication Layer
To generate an authentication layer, we’ll run
mix phx.gen.auth Accounts User users
This command does a lot of the heavy lifting for us:
- It creates an
Accounts
context, which will house our authentication logic. - It generates a
User
schema to model our user data. - The pluralized
users
indicates the name of the corresponding database table.
After running the command we may see the prompt "Do you want to create a LiveView based authentication system?"
. Type Y
and click Enter
.
Next, we’ll install the dependencies that our generated code requires. Run:
mix deps.get
Running migrations
If we try to start our app at this point, we’d likely encounter an error message about pending migrations.

Migrations are like blueprints for our database – they define how our tables should be created and modified. To align our database with our code, we’ll run:
mix ecto.migrate
Exploring the Generated Authentication Views
Let’s examine the views and routes created by our authentication generator to understand how users will register and log in to our app.
Our lib/anon_messages_web/router.ex
file now includes new authentication routes. Let’s break down a few key lines:
live "/users/register", UserRegistrationLive, :new
: This maps the"/users/register"
route to theUserRegistrationLive
LiveView, rendering the registration form.live "/users/log_in", UserLoginLive, :new
: Similarly, this maps the"/users/log_in"
route to theUserLoginLive
LiveView, rendering the login form.
Customizing Our Views
We can find the HTML that determines the appearance of our registration and login pages within the render
functions of UserRegistrationLive
(lib/anon_messages_web/live/user_registration_live.ex
) and UserLoginLive
(lib/anon_messages_web/live/user_login_live.ex
) respectively. We can modify these to align with our design preferences.
Notice also the new “Register” and “Login” links displayed at the top right corner of the homepage and other pages. They were added by the generator to our root layout (lib/anon_messages_web/templates/layout/root.html.heex
):
This code dynamically displays different links depending on whether a user is logged in:
- Logged in: We’ll see the user’s email, “Settings,” and “Log out” links.
- Not logged in: We’ll see the “Register” and “Log in” links.
Linking the “Get started” Button to the Register Page
We can make the “Get Started” button on our homepage direct users to the registration page by updating the code in lib/anon_messages_web/controllers/page_html/home.html.heex
. We’ll wrap the “Get started” button with a <.link></.link>
component that routes to "/users/register"
:
Let’s visit our registration page, create an account, and experiment with logging in and out. We can also explore the provided password reset functionality.
Creating the User’s Messages Page
In this section, we’ll build the /users/messages
route, where logged-in users can see messages that have been anonymously sent to them. They can also see a unique shareable link. We’ll render React components within the LiveView that this route renders.
We’ll begin by adding the following route in lib/anon_messages_web/router.ex
within the scope
that uses the :require_authenticated_user
plug. This ensures that only authenticated users can access this route:
Next, we’ll create the UserMessagesLive
module (lib/anon_messages_web/live/user_messages_live.ex
) and render a container for the React component we’ll create:
Adding React Components to the Phoenix Application
The real magic happens when we connect React to our LiveView. We’ll add a client-side hook in assets/js/app.jsx
that renders our React component when the container element is mounted, and unmounts it when removed:
We define the UserMessages
hook, which is an object with some lifecycle callbacks:
mounted
is invoked when the LiveView element containing thephx-hook="UserMessages"
attribute is added to the DOM (i.e., is rendered on the page). We call themount
function. This function will handle the actual rendering of our<UserMessages/>
React component within the designated container and return another function responsible for unmounting the React component. We store this unmount function inthis.unmount
for later use.destroyed
is executed when the LiveView element is removed from the DOM. Here, we usethis.unmount
, if it exists, to unmount the mounted React component.
We may get a warning that requires us to use the jsx
file extension for files that have JSX content. Change assets/js/app.js
to assets/js/app.jsx
. We also need to inform our Phoenix configuration (config/config.exs
) of this update:
We’ll replace js/app.js
with js/app.jsx
. After editing the config file, we need to stop the server with CTRL + C
and restart the server with mix phx.server
.
Next, let us create this mount
function in assets/js/mount.jsx
:
This is what this mount
function does:
- It starts by getting a reference to the DOM element where we want to render the React component. It does this using
document.getElementById(id)
. - The
createRoot
function fromreact-dom/client
is used to create a React root. A React root is essentially a container within the DOM where React will manage the component tree and updates. root.render(...)
is the key step where the provided Reactcomponent
is rendered inside the React root. The<React.StrictMode>
wrapper is optional but helps with potential issue detection during development.- The
mount
function returns another function. This returned function when called, removes the component and its associated updates from the DOM. This is the function we call in thedestroyed
callback of ourUserMessages
hook.
Next we create the <UserMessages />
component at assets/js/components/UserMessages.jsx
:
Finally, we’ll install the required dependencies for React. From the root directory, navigate to the assets/
directory and install react
and react-dom
:
cd assets && npm i react react-dom
With these done, when we log in and navigate to the /users/messages
route, we should see the rendered <UserMessages />
component.
Creating the User Messages Interface
Let’s update the <UserMessages />
component to display the user’s messages and their unique shareable link. Update the content of assets/js/components/UserMessages.jsx
with this:
We make the <UserMessages />
component to accept userId
and messages
props. We form the user’s shareable link with userId
. We’ll fetch this user ID later from the LiveView. Then we loop through the messages
list and display each message content alongside the time it was sent. We use the formatTime
function created at the end of this file to format this time. Finally, we display the link the user can share to receive messages.
Fetching the Authenticated User
Thanks to our authentication system, accessing the logged-in user is easy. Recall that in lib/anon_messages_web/router.ex
, we place our /users/messages
route within the :require_authenticated_user
live session:
Any route that is placed within this live session invokes the on_mount
function defined in the AnonMessagesWeb.UserAuth
module whose first argument matches :ensure_authenticated
. We can see that in lib/anon_messages_web/user_auth.ex
:
This function calls mount_current_user
on the socket
and session
and assigns the result to socket.assigns
. Then it checks if socket.assigns.current_user
is truthy. If truthy, the connection continues. Otherwise, the connection halts, displaying an error message and redirecting to the login page. Let’s examine the contents of the mount_current_user
function (also within the same file):
This function gets the logged-in user using the user_token
in the session
. Then it calls Phoenix.Component.assign_new
to create a new :current_user
key in socket.assigns
and assigns this logged-in user to it.
Passing the User ID to the Client
In lib/anon_messages_web/live/user_messages_live.ex
, we’ll pass the user ID to the container element as a value for the data-userid
attribute:
Now we can retrieve the user ID in assets/js/app.jsx
and pass it to the React component:
We get the value of the data-userid
attribute using this.el?.dataset?.userid
and we pass it to the userId
prop that the <UserMessages />
component accepts.
With these done, we can refresh our browser and our app should be looking almost done:

💡 You can style any of these pages as you like. You may notice that the Tailwind classes applied to the
<UserMessages />
component are not reflected in the browser. If that’s the case, check the tailwind config (assets/tailwind.config.js
) and update it to also search through.jsx
files for utility classes. Update thecontent
key as so:
content: ["./js/**/*.{js,jsx}", "../lib/*_web.ex", "../lib/*_web/**/*.*ex"]
What’s left for our app is sending anonymous messages and the user receiving those messages
The remaining features for our app are sending anonymous messages and allowing logged-in users to view them.
Sending Anonymous Messages
Creating the Message Sending Page
In this section, we’ll build the page where anyone can send anonymous messages without having to log in.
We’ll start by adding a new route, /send_messages/:recipient_id
in lib/anon_messages_web/router.ex
. Since we want this page to be accessible without logging in, we’ll not add this route to the scope or live session that requires the user to be authenticated. We’ll create a new scope in lib/anon_messages_web/router.ex
and a live session we’ll call ensure_recipient_exists
:
We’ll then implement the on_mount
callback that matches ensure_recipient_exists
in AnonMessagesWeb.UserAuth
(lib/anon_messages_web/user_auth.ex
):
Here, we retrieve the user_id
from the route parameters. Then we call mount_user_by_id
, which retrieves the user with that ID and assigns it to socket.assigns.user_by_id
. If a valid user is found, we make it available via socket.assigns.recipient
and continue the connection. Otherwise, we halt the connection, display an error message to the user, and redirect them to the homepage. Any route defined within this ensure_recipient_exists
live session will automatically trigger this callback.
Next, we’ll create the SendMessagesLive
module which we mapped the /send_messages/:recipient_id
route to. Create the file at lib/anon_messages_web/live/send_messages_live.ex
:
Here, we pass the user’s email and ID as attributes to the SendMessages
container which we’ll retrieve in the hook and pass to the <SendMessages />
component we’ll create.
Create the SendMessages
hook in assets/js/app.jsx
:
Finally, we’ll create the <SendMessages />
component at assets/js/components/SendMessages.jsx
:
Here, we display a form with a text area and a button.
Generating the Messaging Context
To store the anonymous messages and ensure the recipient can see them upon login, we need a schema to model individual messages. We’ll call the schema Message
. Then we need a context module that’ll contain functions to handle message-related operations. We’ll call this context Messaging
.
Phoenix provides a generator we can easily use to generate a context. In the root folder of the project, run the following command:
mix phx.gen.context Messaging Message messages content:string recipient_id:integer
Let’s quickly break down the arguments:
Messaging
is the name of the context module.Message
is the name of the schema module.messages
is the pluralized table name in the database.content:string
: A field to store the message text.recipient_id:integer
: A field to store the user ID of the message’s intended recipient
The generator creates several files, including:
lib/anon_messages/messaging/message.ex
: This contains theMessage
schema and associated functions.lib/anon_messages/messaging.ex
: This is theMessaging
context module containing functions to perform operations on messages.
Finally, we’ll run migrations to inform our database of this new schema. Run:
mix ecto.migrate
Saving Anonymous Messages
Now that we have our context, let’s save the messages when the send form is submitted. In lib/anon_messages_web/live/send_messages_live.ex
, we’ll add a handle_event
function:
This calls the create_message
function from the Messaging
context to create the message and provides feedback to the user.
Next, we’ll push this "send_message"
event from our form to the LiveView when the form is submitted. We’ll start by passing a handleSendMessage
function to the <SendMessages />
component:
This function uses the pushEvent
method to push the "send_message"
event to the containing LiveView, alongside the message parameters as payload.
Finally, we’ll update the <SendMessages />
component to call this handleSendMessage
on form submission:
Displaying User’s Messages
To display a user’s received messages, we begin by creating a function in the Messaging
context (lib/anon_messages/messaging.ex
) to retrieve messages by recipient_id
:
This query fetches a user’s messages and sorts them with the newest messages displayed first.
Next, we’ll update lib/anon_messages_web/live/user_messages_live.ex
to implement a handle_event
callback to respond to the "get_messages"
event:
This fetches messages using our new context function and prepares them in a format suitable for the React component.
This fetches the messages sent to the logged in user using our new context function, and since they are a list of structs, we convert them to a list of maps that React will understand.
Finally, we’ll modify the UserMessages
Hook in assets/js/app.jsx
:
This triggers the message retrieval when the component mounts and passes the retrieved messages
to the <UserMessages/>
component.
With these changes, when a user logs in and accesses their messages, they should see the anonymous messages they’ve received!
For the complete codebase of this project, please refer to the GitHub repository: https://github.com/Steph-crown/anon_messages.
Conclusion
In this tutorial, we’ve built a solid foundation for an anonymous messaging application within a Phoenix LiveView project. Here’s a quick recap of what we’ve accomplished:
- Authentication: Set up a secure user authentication system.
- Message Routing: Created routes for sending and receiving messages.
- Context and Schema: Implemented a
Messaging
context andMessage
schema to work with message data. - User Interface: Built React components to display messages and handle sending new ones.
Here are a few ideas for taking this project further:
- Advanced Features: Consider options like message deletion, replies, or even image attachments.
- Read Indicators: Add a way to mark messages as “read”.
- Custom Styling: Make the message components more visually appealing.