This tutorial series explores how Pusher Channels publish with info API extension makes it easy to determine when a user is offline, and then fallback to notifications via Beams so that they are kept up to date and you can bring them back to your app.
If you have missed the first part of this tutorial series and would like to start from the beginning, please refer to Part 1. Setting up your environment and building authentication with GitHub SSO.
The next thing we are going to do is add a Channels capability to our app. By the end of this section you will have an authentication endpoint running on your server that will allow your app to subscribe to a private-channel.
You will then be able to trigger notifications to this channel from the Pusher debug console and this will update a table with a new notifications.
In your server.js file the first thing to do is initialise the Channels server SDK. We already installed the dependency as part of our app setup so we just need to add the following line under our import section (If you do need to install the Channels server SDK in future you can do so by running npm install pusher
):
1//Imports 2const ChannelsNotifications = require('pusher');
We then need to setup a new Channels client. The Channels client requires an appId, key, secret and cluster which we set in our .env file earlier. We will also add a variable for the relative path to the channels auth endpoint which we will use shortly. Add the following config under //Channels config
and then declare a new channelsClient
:
1//Channels config 2 3let appId = process.env.APP_ID 4let key = process.env.APP_KEY 5let secret = process.env.CHANNELS_SECRET_KEY 6let cluster = process.env.CLUSTER 7let channelsauthEndpoint='/pusher/channels-auth' 8 9const channelsclientConfig = { 10 appId, 11 key, 12 secret, 13 cluster, 14} 15 16const channelsClient = new ChannelsNotifications(channelsclientConfig);
In our client app we will associate a user to a Channels channel with the following naming convention private-userchannel-<userId>
. This means we need an authentication endpoint that can do the following:
ensureAuthenticated
function we implemented earlier to check that the GitHub token that will be passed in as part of the request to this endpoint is valid.private-userchannel
that the userId suffix in the channel name matches the userId that corresponds with the session token. This checks that not only does the user have a valid token but they are also authorised to join the requested channel based on their userid
. If we committed this check any user with a valid session token could join any user channel.Taking all these requirements into account add the following under Channels Auth
:
1//Channels Auth 2app.post('/pusher/channels-auth', ensureAuthenticated, function(req, res) { 3 // Do your normal auth checks here. Return forbidden if session token is invalid đ 4 const userId = req.user.username; // Get user id from auth system based on session token 5 const channelName = req.body.channel_name; 6 const socketId = req.body.socket_id; 7 var isUserChannel=false; 8 if (channelName.startsWith('private-userchannel')){ 9 isUserChannel=true; 10 } 11 if (isUserChannel && channelName !== 'private-userchannel-'+userId) { 12 res.status(401).send('Inconsistent request'); //If userid does not m 13 } else { 14 const channelsToken = channelsClient.authenticate(socketId, channelName); 15 res.send(channelsToken); 16} 17});
The first thing the auth endpoint does is check the session token is valid with the ensureAuthenticated
step. The userId
is determined by the user profile returned from this step and accessible through req.user.username
. We then check if the channel name is a userchannel
and if it is if the channel name doesnât match the format of private-userchannel-userid
we reject the request.
The final thing we need to do is make some of the channels config available to our client app, and we can do this by passing this to our render function from earlier. Update it as follows:
1//Render 2app.get('/', ensureAuthenticated, function(req, res){ 3 res.render('index', { 4 user: req.user.username, 5 key, 6 cluster, 7 channelsauthEndpoint, 8 }) 9});
You can now restart the server.
Returning to our index.hbs file the first thing we will do is import pusher-js into our app. Under the <!--Imports-->
section add:
1<!--Imports--> 2<script src="https://js.pusher.com/7.0.3/pusher.min.js"></script>
We will then add a simple html table. We will add a row to the table each time we get a new channels message received on our private-userchannel
The first thing to do is to add the following to the html body under <!-- Channels table-->
:
1<!--Channels table--> 2<table id="channelsnotifications"> 3 <tr> 4 <th>Title</th> 5 <th>Message</th> 6 </tr> 7</table>
Currently this will just display a heading row on our homepage. The next thing to do is add the following JavaScript function that we can call to add a row to the table. We will call this function each time we get a new Channels message shortly.
For now the function will find the table by the html id, insert a row after the heading (so newest messages will always be the top row) and insert a cell for the notification title and notification text. Additionally we will style the row with a red indicator if the message is tagged as high priority. We will use this information in our fallback mechanism later.
1// Notification table 2function tableUpdate(data){ 3 var table = document.getElementById('channelsnotifications'); 4 var row = table.insertRow(1); 5 var cell1 = row.insertCell(0); 6 if (data.highPriority === true) { 7 cell1.id='highPriority'; 8 }else{ 9 cell1.id='lowPriority'; 10 } 11 var cell2 = row.insertCell(1); 12 cell2.id='noPriority'; 13 cell1.innerHTML = data.notificationTitle; 14 cell2.innerHTML = data.notificationText; 15}
To initialise channels in our JavaScript we need to firstly set our config variables; by using the config passed in to our template by the render function we modified above:
1// Constants 2const userId="{{ user }}" 3const appKey = "{{ key }}" 4const cluster = "{{ cluster }}" 5const channelsauthEndpoint = "{{ channelsauthEndpoint }}"
We can then initialise a new Channels client. This will initiate a new websocket connection to the cluster where our app is located.
1// Channels Initialisation 2 3const pusher = new Pusher(appKey, { 4 cluster: cluster, 5 authEndpoint: channelsauthEndpoint 6});
Under this we need to subscribe to a channel and then bind to messages that contain the event name ânotificationâ:
1//Subscribe channel and bind to event 2 const channel = pusher.subscribe('private-userchannel-'+userId); 3 channel.bind('notification', function(data) { 4 tableUpdate(data); 5 });
What happens here is that calling pusher.subscribe will start subscription request to your userid channel. Because the channel is prefixed with âprivateâ this will automatically perform an authentication callback to your authentication endpoint that we setup earlier. If the authentication request is successful the client will pass an authentication token to the Channels API and the Channels API will complete the subscription request. Any messages then sent to the channel will be delivered to the client.
The channel.bind step is important, because if the client doesnât explicitly bind to messages by the event name the messages will just be unprocessed. By binding to the ânotificationâ event name any messages with that event name will be processed. In this case for each message received the tableUpdate function will be called and the message will be added as a new row in our table.
Finally to test that this is all working as expected, open up a web browser and navigate to your index page. In another tab open up the Channels debug console, by logging into the Channels dashboard, finding your channels app and selecting the the Debug console. Then send a notification as follows:
Channel: private-userchannel-<userid>
userid
should be your GitHub username and should be the id shown on the logout button created earlier
Event: notification
Data:
1{ 2"notificationTitle":"Hello world!", 3"notificationText": "You have a new message", 4"highPriority": true 5}
This should look as follows:
Finally hit Send event and return to the index page. You should see the following.
Congratulations you are now sending realtime messages!
If you send another message with highPriority: false what happens?
Continue to the final part of this tutorial series to learn how to receive web push notifications by associating your GitHub User ID using Beams authenticated users, and re-engage offline users.