The Realtime API is used to send messages in realtime over channels that your site visitors are subscribed to. In this tutorial, we demonstrate the usage of the Realtime API by sending breaking news alerts to visitors on a news site. We allow visitors who are members of our site to decide what types of news alerts they receive. Alerts are sent using an admin page where the admin can choose what type of alert to send.
You can see the final product using this template. To see the site's functionality in action, you need to publish the site and have it open twice. In one instance of the site you act as a site visitor or member and in the other instance you act as the admin.
Let's take a quick look at the final product before we dive into the details of how it works. The site contains three main user-facing parts: the home page, subscriptions lightbox, and admin page.
Home Page
The home page of our news site contains our site's title and links to news stories. We've also added two features to this page that pertain to the breaking news functionality we're building into the site.
The first feature is the breaking news display. This is where we display breaking news alerts sent using the Realtime API. This feature is hidden until it is needed. The display is built from a strip with two text elements. The first text element is a label and the second one is where we populate the text of breaking news alerts. The containing strip is set to Hidden using the Properties & Events panel. It is shown once a breaking news alert is received.
The second feature is a settings icon. This feature is hidden if the site visitor is not logged it. It appears next to the members area login bar. It allows site members who are logged in to open the subscriptions lightbox.
Subscriptions Lightbox
The subscriptions lightbox is where site visitors who are logged in set which types of breaking news alerts they want to receive. The list of alert types is presented using a checkbox group that is populated from data stored in a database collection. That means the list can easily be changed by simply adding or removing items in a collection.
Admin Page
Breaking news alerts are sent using the admin page. For each alert, the admin enters the alert text, what color the alert will be shown in, and which subscribers will receive the alert. Once again, the list of alert types is presented using a checkbox group that is populated from collection data.
The Realtime API provides functionality for sending messages from a publisher to subscribers. To send a message, the sender publishes the message on a specific channel or channel resource. To receive a message, a recipient subscribes to a channel or channel resource. Each time a sender publishes a message, all the recipients who have subscribed to that channel or channel resource receive the message. When recipients subscribe to a channel or channel resource they also define what should be done with the messages when they are received.
Let's see how we apply these concepts to our news site. The publisher in our site is the admin. The admin publishes breaking news alerts. We have multiple types of alerts, such as weather alerts, political updates, and regional news. Each of these types is its own channel resource. So when the admin publishes an alert it is published on one of the channel resources. Our site's members subscribe to receive specific alerts. Each alert type that they subscribe to is one of the channel resources that the admin publishes to. So when the admin publishes a weather alert on the weather channel resource, each member that is on the site at that time and is subscribed to the weather channel resource receives the alert message.
Channels and Channel Resources
Why do we keep talking about a channel or channel resource? What is the difference between a channel and a channel resource? Why would I use one over the other? These are all good questions that we'll answer now.
One difference is how we identify channels and channel resources. Channels only have a name. Channel resources have a channel name and a resource ID. So a channel might be named "visitors", where a channel resource might have the channel name "members" and the resource ID "weather".
Although they are named differently a channel resource's use is very similar to that of a channel. You can publish on and subscribe to both channels and channel resources. Note though that in terms of publishing and subscribing a channel resource is not related in any strict sense to a channel of the same name.
To illustrate this point, let's consider what happens if I have a channel and a channel resource. Let's say my channel is named "myChannel". And let's say my channel resource has the same channel name, "myChannel", but also has the resource ID "myResource". The following rules apply when publishing and subscribing:
So why would you use a channel resource if it seems to be the same as a channel? That is answered by the second difference between a channel and a channel resource. When setting a permissions policy, you can set a policy for each individual channel and channel resource, or you can set a policy that applies to a channel and all its resources. If a channel resource does not have its own explicit policy, it inherits the policy of its channel.
To understand this point, let's look at the channels we use in our site. We have one channel named "visitors", that we want to use for visitors who are not members of our site. We also have another channel, named "members", that has many channel resources, such as "weather" and "politics". We want to restrict access to our "members" channel resources by defining permissions. We do not need to define specific permissions for each "members" channel resource. Instead, we define one permissions policy for the "members" channel and all its channel resources inherit those permissions. Note that if we want to have a special member channel with even more restrictive permissions we could create a "members" channel resource and override the default "members" permissions or we could choose to create another channel with its own permissions policy.
Now let's take a look at how we store the data needed to make our site work.
First, let's consider the breaking news alerts that we send our site members. We want to have a list of the different types of alerts that users can subscribe to. We also want to be able to easily change this list, adding or removing alert types as necessary. To achieve this we store all the alert information in a collection, named SubscriptionTypes, where each item represents an alert type that site members can subscribe to. The items in this collection correspond to the list of alert subscription types that we saw in the subscriptions lightbox and admin page.
Next, because we're creating a site where each member can choose which alert types they want to subscribe to, we need to store each member's subscriptions. To do so we have another collection, called Subscriptions, where each item in the collection represents a site member and that member's subscriptions.
Since we're going to implement our subscriptions using the Realtime API, our data model reflects the entities of that API, such as channels and channel resources. Each of our subscription types is implemented using a realtime channel resource (more details about this below). So when we store a subscription type we need to store the realtime channel resource's channel name and resource ID.
Now let's take a look at the fields in our collections. Notice that we use reference fields to create a relationship between the two collections.
SubscriptionTypes Collection
Subscriptions Collection
The Subscriptions collection is where each member's selected alert subscriptions are stored. Each item represents a site member.
The collection contains the following fields:
Let's begin our discussion of the site's code by looking at the code we've placed in the backend. In the backend we've added code for publishing realtime messages.
realtime.jsw
The first backend file we have is a web module we've named realtime.jsw. We use a web module here because this code needs to be called from the frontend, specifically the admin page. This file contains code for publishing messages on the realtime channels and channel resources that our site members subscribe to.
import { publish } from 'wix-realtime-backend';
export function publishMessage(name, resourceId, message, color) {
const now = new Date();
const channel = {name, resourceId};
const payload = {message, color, time: now.toLocaleTimeString('en-US')};
return publish(channel, payload);
}
As you can see, the file contains one function. We call that function from the admin page to publish messages that are received by subscribers on the home page. The function simply takes in some information and packages it up so it can be sent using the Realtime API.
When calling the
publish()
function from the Realtime API we need to provide it with where we want to publish and what we want to publish. So we take the name
and resourceId
(if there is one) that were passed into the publishMessage()
function and package it up as a Channel
object. We also take the information that we want to publish and package it in an object to be sent as the payload. Here we send the textual message
, the color
that we want the message to be displayed in, and the time
the message was sent.realtime-permissions.js
The second backend file we have is named realtime-permissions.js. This is not a web module because this code is not called from the frontend. This is a special file that needs to be named realtime-permissions.js. It contains code that implements permissions checks for our realtime channels. Each time a site visitor attempts to subscribe to one of our channels the Realtime API calls the functions in this file to see if the visitor has the permissions required to subscribe to the requested channel.
import { permissionsRouter } from 'wix-realtime-backend';
permissionsRouter.default((channel, subscriber) => {
return { 'read': true };
});
const membersChannel = {'name': 'members'};
permissionsRouter.add(membersChannel, (channel, subscriber) => {
if (subscriber.type === 'Member' || subscriber.type === 'Admin') {
return { 'read': true };
} else {
return { 'read': false };
}
});
export function realtime_check_permission(channel, subscriber) {
return permissionsRouter.check(channel, subscriber);
}
In our code, we've elected to use the permissions router, which allows you to create permissions policies in an organized manner.
Let's analyze this code one part at a time.
First, we use the
default()
function to set the default permissions for all channels and channel resources. In our case, we set the default permissions to allow anyone to read. We do so by returning the permissions policy from the callback function passed when calling default()
.permissionsRouter.default((channel, subscriber) => {
return { 'read': true };
});
In our site, we have a "visitors" channel that we subscribe non-members to. Since we don't specify specific permissions for that channel, it receives the default permissions and anyone can subscribe to it.
Next, we create a
Channel
object to represent our "member" channel and use the add()
function to add permissions for it. Since we don't specify specific permissions for each resource in the "members" channel, all the resources inherit the permissions we define here.const membersChannel = {'name': 'members'};
permissionsRouter.add(membersChannel, (channel, subscriber) => {
if (subscriber.type === 'Member' || subscriber.type === 'Admin') {
return { 'read': true };
} else {
return { 'read': false };
}
});
Here again, we specify the permissions by returning them from a callback function. In this case, we check if the user trying to subscribe to a "member" channel is a site member or the site admin. If so, we grant them read permissions. If not, we deny them read permissions.
Finally, we define the
realtime_check_permission()
function. This is the function that gets called each time someone tries subscribing to a channel or channel resource. It gets passed which channel someone is trying to subscribe to and who it is that is trying to subscribe. It returns the permissions that are granted to that subscriber for that channel.For example, when user "MsUser" tries to subscribe to channel "SomeChannel" the
realtime_check_permission()
function is called. The channel
argument contains a Channel
object corresponding to "SomeChannel" and the subscriber
argument contains a User
object corresponding to "MsUser". In the implementation of this function you can take into account who the user is and what channel they are trying to subscribe to. Then you return a ChannelPermissions
object that defines what permissions you've granted "MsUser" on "SomeChannel".Technically, this is the only function we need to implement in order to define realtime permissions. We could have crammed all of our permissions logic into the
realtime_check_permission()
function. Instead, we've used the permissions router, which leads to neater code.export function realtime_check_permission(channel, subscriber) {
return permissionsRouter.check(channel, subscriber);
}
At this point, since we used the permissions router to define our permissions policies, all we need to do is have the permissions router check the permissions policy for the current subscriber on the requested channel and return the result. The permissions router will use the rules we defined above to determine which permissions to grant.
Now we can look at our page code and see where the backend functionality we've just discussed is used.
We've already laid much of the groundwork needed to implement our admin page, but there is still a little work to do in the page code itself. Basically, we need to add code to retrieve and display the types of alerts that an admin can publish and actually publish the alerts.
import wixData from 'wix-data';
import {publishMessage} from 'backend/realtime';
$w.onReady(async function () {
let channels = await wixData.query('subscriptionTypes').find();
channels.items.unshift({_id: 'visitors', channelName: 'visitors', type: 'Visitors'});
let options = channels.items.map(channel => ({label: channel.type, value: channel._id}));
$w('#subscriptions').options = options;
$w('#sendButton').onClick( () => {
$w('#sending').show();
$w('#sendButton').disable();
let promises = $w('#subscriptions').value.map( (subscription) => {
let selectedChannel = channels.items.find(channel => channel._id === subscription);
return publishMessage(
selectedChannel.channelName,
selectedChannel.channelResource,
$w('#message').value,
$w('#colors').value
);
} );
Promise.all(promises)
.then( () => {
$w('#sendButton').enable();
$w('#sending').hide('fade');
} );
} );
});
Once again, let's analyze this code one part at a time.
$w.onReady(async function () {
let channels = await wixData.query('subscriptionTypes').find();
channels.items.unshift({_id: 'visitors', channelName: 'visitors', type: 'Visitors'});
let options = channels.items.map(channel => ({label: channel.type, value: channel._id}));
$w('#subscriptions').options = options;
When the page loads, we start by populating the checkbox group that the admin uses to select which channels to publish on. Most of the channel data comes from a query to the SubscriptionTypes collection. However, we also add the option of the "visitors" channel to the list. Once we have our list, we transform each channel item into a checkbox group option object and populate the checkbox group.
We also define what should be done when the send button is clicked.
$w('#sendButton').onClick( () => {
$w('#sending').show();
$w('#sendButton').disable();
First, we show a message to notify the admin that the send is in progress and we disable the send button so the admin doesn't try sending again before the current send is finished.
let promises = $w('#subscriptions').value.map( (subscription) => {
let selectedChannel = channels.items.find(channel => channel._id === subscription);
return publishMessage(
selectedChannel.channelName,
selectedChannel.channelResource,
$w('#message').value,
$w('#colors').value
);
} );
Then we get all the channels that were selected by the admin from the checkbox group's
value
property. For each selected channel, we find the channel's name and resource ID and use it to publish using the function we defined in the backend realtime.jsw file. In addition to the channel information, we also pass the publishMessage()
function the message entered by the admin and the color the admin chose. Promise.all(promises)
.then( () => {
$w('#sendButton').enable();
$w('#sending').hide('fade');
} );
} );
});
Finally, we wait for all the calls to publish messages to resolve so we can enable the send button and hide the sending notification. Now the admin can publish another message.
The messages published on this page are received by site visitors on the home page who are subscribed to the same channels.
On the home page, we need to write code to deal with visitors logging in to the site, subscribing and unsubscribing visitors to and from channels, and handling alerts when they are received. As usual, let's take a look at the code for this page one piece at a time.
onReady( )
import wixData from 'wix-data';
import wixUsers from 'wix-users';
import { openLightbox } from 'wix-window';
import { subscribe, unsubscribe } from 'wix-realtime';
$w.onReady(function () {
if (wixUsers.currentUser.loggedIn) {
intializeMember(wixUsers.currentUser.id);
} else {
subscribeToVisitorChannel();
}
wixUsers.onLogin((user) => {
intializeMember(user.id);
unsubscribe({channel: {name: 'visitors'}});
});
});
After the necessary imports, our code defines what to do when the page loads. First we check to see if the current user is logged in. If so, we call a function to initialize the page for the member experience. We'll take a look at the details of what that entails below. If the current user is not logged in, we call a function to subscribe the user to the "visitors" channel.
We also define what happens when a user who was not logged in logs in. Again, we call a function to initialize the page for the member experience. We also unsubscribe the member from the "visitors" channel.
Before we take a deep dive into the
intializeMember()
function, let's take a look at the simpler subscribeToVisitorChannel()
function.subscribeToVisitorChannel( )
This function is used to subscribe visitors to the "visitors" channel.
First, we create a
Channel
object to represent the "visitors" channel. Notice that we only use a name
and not a resourceId
because we don't have multiple visitor alert types.Then we call the
subscribe()
function from the Realtime API. The subscribe()
function takes two arguments. The first is a Channel
object and the second is a callback function to call each time a message has been published on that channel. So in this call to the subscribe()
function, each time a message is published to the "visitors" channel we want to call the showBreakingNews()
function to handle the incoming message.Now let's take a look at how the
showBreakingNews()
function works.showBreakingNews( )
This function is used to display the breaking news alerts that have been received from channels.
function showBreakingNews({ payload }) {
$w('#breakingText').html = `<h6><span style="color:${payload.color}">(${payload.time}) ${payload.message}</span><h6>`;
$w('#breakingStrip').show("fade");
}
First, we take the received payload and format it in the style we want for our breaking news alert. Remember from our discussion of the backend code that the payload contains a
message
, color
, and time
. Here we use the html
property of a text element so we can change the color of the alert using the style
attribute and populate the time
and message
as stylized text.Then, all we have to do is show the strip containing our text element.
Now that we've seen how we handle visitors who are not logged in, let's see how we handle members that are logged in.
intializeMember( )
This function is used to set up the page for members who are logged in and to subscribe them to all the alerts that they've set.
async function intializeMember(userId){
let subscriptions = [];
$w('#settings').show();
const initialSubscriptions = await getSubscriptions(userId);
subscribeToMemberChannels(initialSubscriptions, subscriptions);
$w('#settings').onClick(() => {
openLightbox('Subscriptions', subscriptions)
.then(({ added, removed }) => {
subscribeToMemberChannels(added, subscriptions);
unsubscribeFromChannels(removed, subscriptions);
});
});
}
First, we create an array named
subscriptions
, that will hold the member's subscriptions. We use this array to keep the current state of the member's subscriptions. Each time the member subscribes to or unsubscribes from a channel we update this array.Next, we show the settings icon using the
show()
function. Remember, that is how site members open the subscription lightbox to set which alerts they want to subscribe to.Then, we get the list of channels the member has subscribed to using the
getSubscriptions()
function. Remember, these are stored in the Subscriptions collection. Once we get the list of channels we call the subscribeToMemberChannels()
function to subscribe the member to each of those channels.Finally, we define what happens when the settings icon is clicked using the
onClick()
function. When it's clicked we open the subscriptions lightbox. We also define here what happens when the lightbox is closed. In our case, when the lightbox closes it returns to the page a list of channels that the member added and a list of channels that the member removed. So we call a couple of functions to subscribe the member to the added channels and unsubscribe the member from the removed channels.Before taking a look at the functions we use to do the subscribing and unsubscribing, let see how we get the channels a member had subscribed to.
getSubscriptions( )
This function is used to get the subscriptions that a member was subscribed to the last time they visited the site.
function getSubscriptions(userId) {
return wixData.queryReferenced('subscriptions', userId, 'types')
.then(({ result: { items } }) => items)
.catch(err => {
wixData.insert('subscriptions', { _id: userId });
return [];
});
}
Remember, the Subscriptions collection has a field name
types
that is a reference to all the subscription types a member is subscribed to. Here, we use the queryReferenced()
function to get those referenced items based on the current member's ID.If the function runs successfully, we know we are dealing with a member that we have already added to our collection. However, if the function's returned promise is rejected, we are assuming that the rejection is caused by the current member being a new member without a corresponding item in the Subscriptions collection. In that case, we insert a new item into the collection where the item's
_id
is the current member's ID and return an empty array because a new member does not have any saved subscriptions.Important:
The function's promise may have been rejected for other reasons, such as connectivity issues. In the interest of simplicity, we don't deal with that possibility in this example.
subscribeToMemberChannels( )
This function is used to create a subscription for all the channels a member has saved in the subscriptions lightbox.
function subscribeToMemberChannels(channels, subscriptions) {
channels.forEach(channel => {
let memberChannel = { name: channel.channelName, resourceId: channel.channelResource };
subscribe(memberChannel, showBreakingNews)
.then(subscriptionId => {
subscriptions.push(channel);
});
});
}
For each channel in the list passed to the function, we create a
Channel
object and call the realtime subscribe()
function to subscribe the member to the channel. Just as we did above, we pass the showBreakingNews()
function as the callback function to be called when a message is received on the channel.If the subscription is successful, we store the channel information in the
subscriptions
array.unsubscribeFromChannels( )
This function is used to unsubscribe a member from channels. It is called after a member deselects a subscription in the subscriptions lightbox.
function unsubscribeFromChannels(removed, subscriptions) {
removed.forEach(id => {
let toRemove = subscriptions.find(subscription => subscription._id === id);
let toRemoveIndex = subscriptions.findIndex(subscription => subscription._id === id);
unsubscribe({ channel: { name: toRemove.channelName, resourceId: toRemove.channelResource } })
.then(() => {
subscriptions.splice(toRemoveIndex, 1);
});
});
}
For each ID of a removed subscription, we find the corresponding object in the
subscriptions
array and the index of where it resides in the array. We use the object data to call the realtime unsubscribe()
function. If the unsubscribe is successful, we use the index to remove the items from the subscriptions
array.In the subscriptions lightbox, we need to write code that allows members to choose which alerts they want to subscribe to and unsubscribe from. As we saw in the home page code, the actual subscribing and unsubscribing happens there. Here, in the subscriptions lightbox, we just have to collect the information and pass it to the home page.
As always, we'll analyze the code one part at a time.
import wixData from 'wix-data';
import wixUsers from 'wix-users';
import wixWindow from 'wix-window';
$w.onReady(async function () {
let userId = wixUsers.currentUser.id;
let selectedIndices = [];
let startValues = [];
let channels = await wixData.query('subscriptionTypes').find();
let options = channels.items.map((channel) => {
return { label: channel.type, value: channel._id }
});
$w('#subscriptions').options = options;
// onReady() continues below...
After the necessary imports and declaration of some variables, the first section of code that runs when the lightbox opens populates the checkbox group in the lightbox with all the possible subscription types. We've already seen this done on the admin page, and the code here is very similar.
Tip:
For performance improvements, try retrieving the subscription types the first time the lightbox opens and then store that information using the wix-storage API for any subsequent times it opens.
// ...onReady() continued from above
let subscriptionIds = wixWindow.lightbox.getContext().map((subscription) => subscription._id);
options.forEach((option, index) => {
if (subscriptionIds.includes(option.value)) {
selectedIndices.push(index);
startValues.push(option.value);
}
});
$w('#subscriptions').selectedIndices = selectedIndices;
// onReady() continues below...
The next section of code that runs when the lightbox opens selects all the options in the checkbox group that the current member has subscribed to. The code first gets the IDs of all the subscriptions passed from the home page when opening the lightbox. Then, for each option in the checkbox group, it checks to see if it exists in the list of subscription IDs. If so, it sets the option to selected.
The code also stores these starting subscriptions in a list. This will be used later when determining if the member selected any new subscriptions. We'll reference our ending subscription list against this starting list.
// ...onReady() continued from above
$w('#save').onClick(() => {
let endValues = $w('#subscriptions').value;
let added = endValues.filter(x => !startValues.includes(x)).map(addedId => {
return channels.items.find(channel => channel._id === addedId);
});
let removed = startValues.filter(x => !endValues.includes(x));
wixData.replaceReferences('subscriptions', 'types', userId, $w('#subscriptions').value);
wixWindow.lightbox.close({added, removed});
});
// end of onReady()
The final section of code that runs when the light box opens defines what happens when the save button is clicked. When it is clicked, we get the list of subscriptions that the member selected. We cross-reference this list against the starting list to determine which subscriptions the member added and which ones the member removed. We update the member's entry in the Subscriptions table to reflect the new subscription statuses and send the list of added and removed subscriptions back to the home page to be handled there.
To learn more about the Realtime API see the wix-realtime and wix-realtime-backend sections of the API Reference.
Previously published at https://support.wix.com/en/article/velo-tutorial-sending-messages-with-the-realtime-api