Although Phoenix is great at many things, handling real-time events is where the framework really shines! Dealing with websockets has traditionally been quite cumbersome for web developers, but Erlang's runtime environment combined with Elixir's ease of use and Phoenix's abstractions make it a breeze. Without further ado, let's dive into it!
In order to be able to test this functionality, we will open a new browser application (so if you're already logged-in on Chrome, use Firefox for example), and create a brand new user. Personally I'm going to use Safari and call the new user "alice". You should then log in and create a new room where both of your users can communicate with each other.
We now want to be able to send a message from our user to "alice", and have that message instantly appear in Alice's window, without her having to manually fetch it!
Creating and connecting to a channel
Phoenix introduces the concept of channels – a websocket endpoint that clients can connect to, in order to send and receive messages. The best way to understand it is through an example. Let's generate a new channel for our rooms!
You should now have a file at
lib/messengyr_web/channels/room_channel.ex. Let's open it and take a look. As you can see, there's a default room called
lobby that anyone can join:
If you scroll down the file, you'll see that
authorized?/1 is a private function that always returns
true, so joining this room will currently always work without any problems.
We also list all of our "channel routes" in the
user_socket.ex-file. Here we can specify what the endpoint that connects to the
RoomChannel should be. In this case, we'll just un-comment the line that Phoenix already has for us near the top of the file, which says that all routes that start with the prefix
room: should go to the room channel:
assets/js/socket.js. If you open this file, you'll find plenty of comments explaining how to use it. Delete those comments so that we can get a better overview of what the file is doing. We'll also delete the part that connects to the channel
"topic:subtopic", since that endpoint doesn't exist. You final file should look like this:
Next, we'll go into
menu-container.js and import this socket file at the top. Using the socket object, we can create a
channel variable, which specifies which Phoenix channel we want to connect to (in this case,
"room.lobby"), and finally we'll join the channel:
...and this in your Phoenix log:
Sending a socket message
Now that we're connected to the socket, we can try to send a message to it! If we open
room_channel.ex again, you'll see that it already has some default
handle_in/3-functions. One of them pattern matches against the string
"shout" – that function takes whatever message it receives and broadcasts it to all the users that are connected to the room.
channel variable that we created earlier and push the string
"shout". We also want to listen to the channel so that we can log something when we receive the "shout":
This might not seem very advanced, but the cool part is that the messages aren't necessarily tied to your own user. Any user connected to the
"room:lobby" channel can broadcast the "shout" message, and it will still be intercepted by you!
We can try this out by opening the other browser where Alice is logged-in (Safari in my case), and hit refresh multiple times. That way, Alice will send out multiple shouts, and you'll notice that your own user will log every single one!
Before we start sending and receiving more messages, we want to make sure that our websocket endpoint requires authentication, so that only logged-in users can use it. For this, we'll use Guardian and our JWT-token again.
If you re-open the autogenerated
socket.js-file, you'll see that Phoenix is trying to guide us in the right direction by sending a
window.userToken variable under the key
"token" when it connects to the socket endpoint. This
userToken is undefined however, but we do have the global variable
jwtToken which we used in our Fetch requests earlier. Let's alter the file so that it uses that instead:
Now, back in
user_socket.ex, we're going to use the
resource_from_claims/1-function from our
Messengyr.Auth.Guardian module to decode the token and retrieve the user that belongs to it. We'll do this in the user socket's
connect/2-function, which is called as soon as the user attempts to connect to the socket. After we've retrieved the user, we'll assign it to the
socket variable so that we can easily read it later. If the user isn't logged in, or we cannot retrieve the user for whatever reason, we'll return an
Here you can also clearly see the benefit of using Elixir's
with – it allows us to easily pattern match multiple conditionals one after the other. If we wanted to write the same logic using `case` statements, things would get deeply nested:
We'll also define a second
connect/2-function that doesn't pattern match against
guardianToken. If the user tries to connect without that parameter, it should return an
If you refresh the page, you should be able to use the socket just like before. However, you can try experimenting with the
socket.js-file and comment out the
guardianToken line. You'll notice that, if you don't send the token, or if you change it to something that's not valid, you won't be able to connect to the socket (which is the expected behaviour)!
Connecting to a specific room
Alright, enough with the "shouting" in the "lobby"! What we really want to do is to connect to each room where the user is a member. Instead of connecting to
"room:lobby", a user could then connect to the channels
"room:2", provided that they are a member of the rooms with ID 1 and 2.
Let's start with the
room_channel.ex-file. We'll remove all the functions that are currently in it, and instead have a single
join/3-function that connects to a room with a specific ID:
Next, we'll go to
menu-container.js and delete the code that's related to joining the "lobby" channel. Instead, we'll create a
getRoomChannel-function that takes a
roomId, connects to the channel of that room, and returns the channel:
Finally, we'll loop through all of our rooms (right after fetching them with AJAX in
componentDidMount) and add a
channel to each room object before putting them in our Redux store:
Refresh the page, and you should now see your user join only the rooms that you're a member of in the console:
What we have is pretty good, however, we should handle cases where the user tries to connect to a room that doesn't exist. Also, what happens if another logged-in user manually connects to the channel
"room:10" to spy on the conversation I'm having with Alice? We obviously can't let that happen, so we need the
room_channel.ex to make sure that the room exists, and only let in the members of that specific room.
To do that, we'll first need two new functions in our chat context:
Using these new functions, we can now update the room channel's
There we go! Now it should be safe from eavesdroppers. To test that we cannot connect to just any random room, you can try to call the
getRoomChannel-function from React with a room ID that doesn't exist (like
You should then see an error message for that room in your log:
Now you know how to connect to a websocket in Phoenix and perform some basic authentication in your channels! In the next chapter, we'll make our two users communicate with each other through this channel.