Velo Tutorial: Sending Messages with the Realtime API

Note: This tutorial and its steps are based on a Wix Editor site. You can adapt the steps for a Wix Studio site by using the equivalent Wix Studio features.

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.

Site Overview

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 visitor-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.

Realtime API Overview

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:

  • Messages published on "myChannel" are not published on "myChannel:myResource" and vice versa.
  • If you subscribe to "myChannel", you will not receive messages from "myChannel:myResource" and vice versa.

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. 

Data Model

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 members 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

  • Type (type): The display name of the subscription type. This shows in the subscription lightbox and admin page.
  • Channel Name (channelName): Name of the realtime channel for each subscription. (You might notice that all of our channels have the same name, "members". We store this information in case we want to expand our site to use multiple channels in the future.)
  • Channel Resource (channelResource): Name of the realtime channel resource for each alert.
  • Subscriptions (subscriptions): A multi-reference field that points to the members in the Subscriptions collection who subscribe to each alert. (Here the members show as "Untitled" because of the unused Title field described below.)

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:

  • Title (title): This field is not used to store data. However, we need to keep it because none of our other fields can serve as the primary field.
  • ID (_id): ID of a site member. Note that usually when using collections the ID field is automatically generated. Here we take advantage of the fact that you can assign a specific ID to an item if you create the item using the Wix Data API. So when we create a new member item in this collection we set the ID field to the member's personal ID.  
  • Types (types): A multi-reference field that points to the subscription types in the SubscriptionTypes collection that each member subscribes to.

Backend Code

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.web.js

The first backend file we have is a web module we've named realtime.web.js. 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.

Copy
1
import { Permissions, webMethod } from 'wix-web-module';
2
import { publish } from 'wix-realtime-backend';
3
4
export const publishMessage = webMethod(Permissions.Anyone, (name, resourceId, message, color) => {
5
const now = new Date();
6
const channel = {name, resourceId};
7
const payload = {message, color, time: now.toLocaleTimeString('en-US')};
8
9
return publish(channel, payload);
10
});

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.

Copy
1
import { permissionsRouter } from 'wix-realtime-backend';
2
3
permissionsRouter.default((channel, subscriber) => {
4
return { 'read': true };
5
});
6
7
const membersChannel = {'name': 'members'};
8
9
permissionsRouter.add(membersChannel, (channel, subscriber) => {
10
if (subscriber.type === 'Member' || subscriber.type === 'Admin') {
11
return { 'read': true };
12
} else {
13
return { 'read': false };
14
}
15
});
16
17
export function realtime_check_permission(channel, subscriber) {
18
return permissionsRouter.check(channel, subscriber);
19
}

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().

Copy
1
permissionsRouter.default((channel, subscriber) => {
2
return { 'read': true };
3
});

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.

Copy
1
const membersChannel = {'name': 'members'};
2
3
permissionsRouter.add(membersChannel, (channel, subscriber) => {
4
if (subscriber.type === 'Member' || subscriber.type === 'Admin') {
5
return { 'read': true };
6
} else {
7
return { 'read': false };
8
}
9
});

Here again, we specify the permissions by returning them from a callback function. In this case, we check if the visitor 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 visitor "MsVisitor" 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 an object corresponding to "MsVisitor". In the implementation of this function you can take into account who the visitor is and what channel they are trying to subscribe to. Then you return a ChannelPermissions object that defines what permissions you've granted "MsVisitor" 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.

Copy
1
export function realtime_check_permission(channel, subscriber) {
2
return permissionsRouter.check(channel, subscriber);
3
}

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.

Admin Page Code

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.

Copy
1
import wixData from 'wix-data';
2
import {publishMessage} from 'backend/realtime.web';
3
4
$w.onReady(async function () {
5
let channels = await wixData.query('subscriptionTypes').find();
6
channels.items.unshift({_id: 'visitors', channelName: 'visitors', type: 'Visitors'});
7
let options = channels.items.map(channel => ({label: channel.type, value: channel._id}));
8
$w('#subscriptions').options = options;
9
10
$w('#sendButton').onClick( () => {
11
$w('#sending').show();
12
$w('#sendButton').disable();
13
let promises = $w('#subscriptions').value.map( (subscription) => {
14
let selectedChannel = channels.items.find(channel => channel._id === subscription);
15
return publishMessage(
16
selectedChannel.channelName,
17
selectedChannel.channelResource,
18
$w('#message').value,
19
$w('#colors').value
20
);
21
} );
22
23
Promise.all(promises)
24
.then( () => {
25
$w('#sendButton').enable();
26
$w('#sending').hide('fade');
27
} );
28
} );
29
});

Once again, let's analyze this code one part at a time.


Copy
1
$w.onReady(async function () {
2
let channels = await wixData.query('subscriptionTypes').find();
3
channels.items.unshift({_id: 'visitors', channelName: 'visitors', type: 'Visitors'});
4
let options = channels.items.map(channel => ({label: channel.type, value: channel._id}));
5
$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.

Copy
1
$w('#sendButton').onClick( () => {
2
$w('#sending').show();
3
$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.


Copy
1
let promises = $w('#subscriptions').value.map( (subscription) => {
2
let selectedChannel = channels.items.find(channel => channel._id === subscription);
3
return publishMessage(
4
selectedChannel.channelName,
5
selectedChannel.channelResource,
6
$w('#message').value,
7
$w('#colors').value
8
);
9
} );

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.web.js 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.


Copy
1
Promise.all(promises)
2
.then( () => {
3
$w('#sendButton').enable();
4
$w('#sending').hide('fade');
5
} );
6
} );
7
});

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.

Home Page Code

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( )

Copy
1
import wixData from 'wix-data';
2
import { authentication, currentMember } from 'wix-members-frontend';
3
import { openLightbox } from 'wix-window-frontend';
4
import { subscribe, unsubscribe } from 'wix-realtime-frontend';
5
6
$w.onReady(async function () {
7
if (authentication.loggedIn()) {
8
let member = await currentMember.getMember();
9
intializeMember(member._id);
10
} else {
11
subscribeToVisitorChannel();
12
}
13
14
authentication.onLogin(async (member) => {
15
intializeMember(member.id);
16
unsubscribe({channel: {name: 'visitors'}});
17
});
18
});

After the necessary imports, our code defines what to do when the page loads. First we check to see if the current visitor 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 visitor is not logged in, we call a function to subscribe the visitor to the "visitors" channel.

We also define what happens when a visitor 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.

Copy
1
function subscribeToVisitorChannel() {
2
const visitorChannel = { name: 'visitors' };
3
return subscribe(visitorChannel, showBreakingNews);
4
}

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.

Copy
1
function showBreakingNews({ payload }) {
2
$w('#breakingText').html = `<h6><span style="color:${payload.color}">(${payload.time}) ${payload.message}</span><h6>`;
3
$w('#breakingStrip').show("fade");
4
}

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.

Copy
1
async function intializeMember(memberId){
2
let subscriptions = [];
3
4
$w('#settings').show();
5
6
const initialSubscriptions = await getSubscriptions(memberId);
7
subscribeToMemberChannels(initialSubscriptions, subscriptions);
8
9
$w('#settings').onClick(() => {
10
openLightbox('Subscriptions', subscriptions)
11
.then(({ added, removed }) => {
12
subscribeToMemberChannels(added, subscriptions);
13
unsubscribeFromChannels(removed, subscriptions);
14
});
15
});
16
}

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.

Copy
1
function getSubscriptions(memberId) {
2
return wixData.queryReferenced('subscriptions', memberId, 'types')
3
.then(({ result: { items } }) => items)
4
.catch(err => {
5
wixData.insert('subscriptions', { _id: memberId });
6
return [];
7
});
8
}

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.

Copy
1
function subscribeToMemberChannels(channels, subscriptions) {
2
channels.forEach(channel => {
3
let memberChannel = { name: channel.channelName, resourceId: channel.channelResource };
4
subscribe(memberChannel, showBreakingNews)
5
.then(subscriptionId => {
6
subscriptions.push(channel);
7
});
8
});
9
}

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.

Copy
1
function unsubscribeFromChannels(removed, subscriptions) {
2
removed.forEach(id => {
3
let toRemove = subscriptions.find(subscription => subscription._id === id);
4
let toRemoveIndex = subscriptions.findIndex(subscription => subscription._id === id);
5
unsubscribe({ channel: { name: toRemove.channelName, resourceId: toRemove.channelResource } })
6
.then(() => {
7
subscriptions.splice(toRemoveIndex, 1);
8
});
9
});
10
}

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.

Subscriptions Lightbox Code

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.

Copy
1
import wixData from 'wix-data';
2
import { currentMember } from 'wix-members-frontend';
3
import wixWindowFrontend from 'wix-window-frontend';
4
5
$w.onReady(async function () {
6
let member = currentMember.getMember();
7
let memberId = member._id;
8
let selectedIndices = [];
9
let startValues = [];
10
11
let channels = await wixData.query('subscriptionTypes').find();
12
let options = channels.items.map((channel) => {
13
return { label: channel.type, value: channel._id }
14
});
15
$w('#subscriptions').options = options;
16
17
// 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-frontend API for any subsequent times it opens.


Copy
1
// ...onReady() continued from above
2
3
let subscriptionIds = wixWindowFrontend.lightbox.getContext().map((subscription) => subscription._id);
4
5
options.forEach((option, index) => {
6
if (subscriptionIds.includes(option.value)) {
7
selectedIndices.push(index);
8
startValues.push(option.value);
9
}
10
});
11
12
$w('#subscriptions').selectedIndices = selectedIndices;
13
14
// 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. 


Copy
1
// ...onReady() continued from above
2
3
$w('#save').onClick(() => {
4
let endValues = $w('#subscriptions').value;
5
6
let added = endValues.filter(x => !startValues.includes(x)).map(addedId => {
7
return channels.items.find(channel => channel._id === addedId);
8
});
9
let removed = startValues.filter(x => !endValues.includes(x));
10
11
wixData.replaceReferences('subscriptions', 'types', memberId, $w('#subscriptions').value);
12
wixWindowFrontend.lightbox.close({added, removed});
13
});
14
15
// 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.

Learn More

To learn more about the Realtime API see the wix-realtime-frontend and wix-realtime-backend sections of the API Reference.

Was this helpful?
Yes
No