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
Accountscontext, which will house our authentication logic. - It generates a
Userschema to model our user data. - The pluralized
usersindicates 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 theUserRegistrationLiveLiveView, rendering the registration form.live "/users/log_in", UserLoginLive, :new: Similarly, this maps the"/users/log_in"route to theUserLoginLiveLiveView, 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:
mountedis invoked when the LiveView element containing thephx-hook="UserMessages"attribute is added to the DOM (i.e., is rendered on the page). We call themountfunction. 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.unmountfor later use.destroyedis 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
createRootfunction fromreact-dom/clientis 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 Reactcomponentis rendered inside the React root. The<React.StrictMode>wrapper is optional but helps with potential issue detection during development.- The
mountfunction 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 thedestroyedcallback of ourUserMessageshook.
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.jsxfiles for utility classes. Update thecontentkey 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:
Messagingis the name of the context module.Messageis the name of the schema module.messagesis 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 theMessageschema and associated functions.lib/anon_messages/messaging.ex: This is theMessagingcontext 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
Messagingcontext andMessageschema 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.