Build match-day chat for fans

This tutorial builds match-day chat for fans on the PubNub JavaScript SDK. It uses Pub/Sub to carry messages, Presence to report who's watching, and App Context to store the channel and fan profiles. It also uses Message Persistence to keep a backlog and Message Actions to attach reactions.

By the end, two Node.js clients exchange messages on one channel, game.chat. Each client shows how many fans are present and loads the recent backlog on start, and a reaction added on one client appears on the other. Presence, Message Persistence, and App Context must all be enabled on your keyset before you start this tutorial.

What you'll build​

Each of two Node.js clients, index.js for fan-42 and index2.js for fan-108, first writes the channel's name and its own profile to App Context. It then subscribes to game.chat with presence events, so Presence reports who joins and leaves, and fetches the recent backlog from Message Persistence. When one fan publishes a message, PubNub delivers it to the other client. When either fan adds a reaction to a message, PubNub delivers that Message Action to both clients without republishing the message.

Before you begin​

You need:

  1. Node.js 22 or later.
  2. A PubNub account and a keyset. A keyset is the set of publish, subscribe, and secret keys that identifies your application to the PubNub network. If you don't have one, follow Set up your account first.
  3. Presence enabled on the keyset. Presence must be enabled on the keyset, and new keysets have it enabled by default. Confirm it in the PRESENCE section of your keyset settings in the Admin Portal.
  4. Message Persistence enabled on the keyset. Message Persistence must be enabled on the keyset before you store or read history, and new keysets don't enable it by default. Turn it on in the MESSAGE PERSISTENCE section of your keyset settings.
  5. App Context enabled on the keyset. App Context must be enabled on the keyset before you call it, and new keysets don't enable it by default. Turn it on in the APP CONTEXT section of your keyset settings.

You also need the publish key and subscribe key from that keyset.

Set up the project​

Create a new directory and install the PubNub JavaScript SDK:

mkdir match-day-chat
cd match-day-chat
npm init -y
npm install pubnub

Add "type": "module" to package.json, since the code on this page uses import. Create a file called index.js, then add the client:

import PubNub from 'pubnub';

const pubnub = new PubNub({
publishKey: 'YOUR_PUBLISH_KEY',
subscribeKey: 'YOUR_SUBSCRIBE_KEY',
userId: 'fan-42',
});

Replace YOUR_PUBLISH_KEY and YOUR_SUBSCRIBE_KEY with the keys from your keyset. This file represents one fan, fan-42. Later, once you reach Give the fan a profile, you'll duplicate it to get a second fan for the other terminal.

Describe the chat channel​

Give game.chat a name and description in App Context, so every client can render the same header without maintaining its own copy of it.

1

Append this call to index.js. App Context is where a channel's name and description live, so any client that reads game.chat's metadata, not only the one that set it, renders the same "Match chat" header. Calling this again later overwrites the same fields, so both fans running it on startup is harmless. Without App Context, every client would need its own hardcoded copy of the channel's name, which drifts the moment one copy changes and another doesn't.

Give the fan a profile​

Give the connecting fan a name and an avatar in App Context, so the other fan can render it next to their messages.

1

This calls Set user metadata for fan-42, the same User ID this file's client uses. To get a second fan for the other terminal, copy index.js to index2.js, then in the copy change userId: 'fan-42' in the client configuration and uuid: 'fan-42' in this call to fan-108. In this tutorial, each fan sets its own profile on startup. In production, a client shouldn't be able to write another fan's profile, since App Context has no owner check of its own. Access Manager's permission model enforces that requirement, granting the update permission on a User ID only to the client authorized to hold it.

Join the chat​

Subscribe to game.chat with presence events turned on, and register a handler for messages and one for presence.

1

receivePresenceEvents asks this subscription for presence events as well as messages. The message handler runs each time the other fan publishes to game.chat. The presence handler runs on join, leave, and the periodic interval update, printing the current fan count each time, so the count you show later stays current without polling. Leaving receivePresenceEvents out still delivers messages normally. It leaves the presence handler silent, with no error to flag the difference.

Send a message​

Publish a chat message to game.chat.

1

The standard message payload size limit is 32 KiB. This includes the channel name and any metadata. A message over that limit fails the publish call instead of arriving trimmed, so keep a chat message to plain text rather than an embedded image or a large payload. storeInHistory: true makes this message available to the history call in the next step. The timetoken this call returns identifies the message uniquely, which matters again in Let fans react to a message.

Load what was said before the fan arrived​

Fetch the recent backlog on startup, so a fan who joins mid-match sees what was already said.

1

This calls Retrieve message history for game.chat. Message Persistence retention is 1 or 7 days on the Free plan, 30 days, 3 months, or 6 months on Starter, and 1 year or Unlimited on Pro, so how far back this call can reach depends on your keyset's configured retention, not on this code. Call it once, right after subscribing, rather than on every render, since the messages it returns don't change until someone publishes a new one.

Show how many fans are in the chat​

Call Here Now once, right after subscribing, to seed the fan count you'll then keep current from presence events.

1

This calls Here Now for game.chat in count-only mode, includeUUIDs: false. That mode keeps the response flat whether the chat has 3 fans or 3,000, since a count is all this step needs.

Call it once on join rather than on a timer. Each Here Now call has a cost, and a tight polling loop pays for each one without adding live accuracy — the presence handler you registered in Join the chat keeps the count current after that first call, at no extra cost per update.

Let fans react to a message​

Add a reaction to a message, then handle the reaction event on both clients.

1

1

The first call attaches a message action to a message that already exists, identified by that message's own timetoken. Replace replace-with-message-timetoken with a real one, for example the timetoken your terminal printed in Send a message. PubNub stores the reaction against that original message. So reacting to it needs no second channel and no bookkeeping table of your own to associate a reaction with the message it belongs to. The second block registers the handler that fires when either fan adds a reaction, so both clients show it without either one re-publishing the original message.

Run it​

Open two terminals in match-day-chat. In the first, run:

node index.js

In the second, run the duplicated file for the other fan:

node index2.js

Each terminal logs the channel metadata it set, its own fan profile, an initial fan count, the recent backlog, and the message it published. Once both are running, each terminal also prints the message the other fan sent. Output resembles this, with your own timetokens:

channel metadata set: { name: 'Match chat', description: 'Open chat for everyone watching the match' }
fan profile set: { name: 'Alex Moreau', profileUrl: 'https://example.com/avatars/fan-42.png', custom: { supports: 'home' } }
fans in the chat: 1
17000000000000000 { text: 'What a save!' }
chat message published at timetoken: 17000000000000000
fans in this chat: 2
fan-108: {"text":"What a save!"}

The reaction call fails on this first run, because replace-with-message-timetoken isn't a real message. Copy a timetoken your terminal printed, paste it into that call, and run node index.js again. This time both terminals print the reaction. Press Ctrl+C in either terminal to stop that client.

What happened​

  1. Each client set game.chat's name and description in App Context, and its own fan profile.
  2. Each client subscribed to game.chat with presence events enabled, then called Here Now once to seed its fan count.
  3. Each client fetched the recent backlog from Message Persistence before reacting to anything new.
  4. Each client published a chat message, which PubNub delivered to the other client's message handler.
  5. One client added a message action to a message, and PubNub delivered that action to both clients' message-action handlers, without a second channel or a republish of the original message.

Next steps​

Was this page useful?

Last updated on