Velo Tutorial: Sending Messages with the Realtime API
Site Overview
Home Page

Subscriptions Lightbox

Admin Page

Realtime API Overview
Channels and Channel Resources
- 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.
Data Model
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 users show as "Untitled" because of the unused Title field described below.)
Subscriptions Collection

- 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
realtime.jsw
1import { publish } from 'wix-realtime-backend';
2
3export function publishMessage(name, resourceId, message, color) {
4 const now = new Date();
5 const channel = {name, resourceId};
6 const payload = {message, color, time: now.toLocaleTimeString('en-US')};
7
8 return publish(channel, payload);
9}
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
1import { permissionsRouter } from 'wix-realtime-backend';
2
3permissionsRouter.default((channel, subscriber) => {
4 return { 'read': true };
5});
6
7const membersChannel = {'name': 'members'};
8
9permissionsRouter.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
17export function realtime_check_permission(channel, subscriber) {
18 return permissionsRouter.check(channel, subscriber);
19}
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()
.
1permissionsRouter.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.
1const membersChannel = {'name': 'members'};
2
3permissionsRouter.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 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.
1export function realtime_check_permission(channel, subscriber) {
2 return permissionsRouter.check(channel, subscriber);
3}
Admin Page Code
1import wixData from 'wix-data';
2import {publishMessage} from 'backend/realtime';
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});
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;
1 $w('#sendButton').onClick( () => {
2 $w('#sending').show();
3 $w('#sendButton').disable();
4
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.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.
1 Promise.all(promises)
2 .then( () => {
3 $w('#sendButton').enable();
4 $w('#sending').hide('fade');
5 } );
6 } );
7});
Home Page Code
onReady( )
1import wixData from 'wix-data';
2import wixUsers from 'wix-users';
3import { openLightbox } from 'wix-window';
4import { subscribe, unsubscribe } from 'wix-realtime';
5
6$w.onReady(function () {
7 if (wixUsers.currentUser.loggedIn) {
8 intializeMember(wixUsers.currentUser.id);
9 } else {
10 subscribeToVisitorChannel();
11 }
12
13 wixUsers.onLogin((user) => {
14 intializeMember(user.id);
15 unsubscribe({channel: {name: 'visitors'}});
16 });
17});
Before we take a deep dive into the intializeMember()
function, let's take a look at the simpler subscribeToVisitorChannel()
function.
subscribeToVisitorChannel( )
1function 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( )
1function 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.
intializeMember( )
1async function intializeMember(userId){
2 let subscriptions = [];
3
4 $w('#settings').show();
5
6 const initialSubscriptions = await getSubscriptions(userId);
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.
getSubscriptions( )
1function getSubscriptions(userId) {
2 return wixData.queryReferenced('subscriptions', userId, 'types')
3 .then(({ result: { items } }) => items)
4 .catch(err => {
5 wixData.insert('subscriptions', { _id: userId });
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.
subscribeToMemberChannels( )
1function 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( )
1function 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
1import wixData from 'wix-data';
2import wixUsers from 'wix-users';
3import wixWindow from 'wix-window';
4
5$w.onReady(async function () {
6 let userId = wixUsers.currentUser.id;
7 let selectedIndices = [];
8 let startValues = [];
9
10 let channels = await wixData.query('subscriptionTypes').find();
11 let options = channels.items.map((channel) => {
12 return { label: channel.type, value: channel._id }
13 });
14 $w('#subscriptions').options = options;
15
16 // onReady() continues below...
1// ...onReady() continued from above
2
3let subscriptionIds = wixWindow.lightbox.getContext().map((subscription) => subscription._id);
4
5options.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...
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', userId, $w('#subscriptions').value);
12 wixWindow.lightbox.close({added, removed});
13});
14
15// end of onReady()