Hide and remove chat messages during a match

This tutorial builds message moderation for match-day chat with Access Manager and Message Persistence on the PubNub JavaScript SDK. By the end, a moderator client publishes a hide decision to a control channel that only a moderator's token can write to, so every connected client hides the flagged message live. A client that joins later still renders it as hidden, because the same control channel's history carries the decision. The message is then deleted from Message Persistence for good. Deleting from history is permanent and needs your keyset's secret key, so that step runs on your own server, never in a client.

A fan's client can publish chat messages and reactions on game.chat. If hiding a message also ran through game.chat, whatever mechanism a fan uses to react could equally be used to fabricate a hide decision, since Access Manager grants permission on a channel, not on the specific type or value a client chooses to put in a message. This tutorial gives the hide decision its own channel instead, one that only a moderator's token can write to, so the permission fans get on game.chat never extends to hiding anyone's message.

What you'll build​

server.js holds the secret key and grants moderator-7 a token with write on game.chat.moderation. moderator.js uses that token to publish a hide decision, naming the flagged message's timetoken, to game.chat.moderation. Every client subscribed to that channel receives the decision and hides the message live. A fan who joins later fetches history from both game.chat and game.chat.moderation and hides it too. Finally, server.js deletes the message from Message Persistence for good.

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. Follow Set up your account if you don't have one yet.
  3. Access Manager enabled on the keyset. Without it, any client holding your keys can already publish anywhere, and there's no way to keep the hide decision moderator-only.
  4. Message Persistence enabled on the keyset, since flagging and deleting a message both depend on it being stored. Turn it on in the MESSAGE PERSISTENCE section of your keyset settings in the Admin Portal.
  5. Enable Delete-From-History turned on in the same section. Delete messages from history returns an error if this toggle is off.

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

Set up the project​

Create a directory and install the SDK:

mkdir chat-moderation
cd chat-moderation
npm init -y
npm install pubnub

Add "type": "module" to package.json. Create two files: moderator.js, the moderator's own client, and server.js, which runs on your own infrastructure and holds the keyset's secret key. Only server.js ever sees the secret key.

moderator.js:

import PubNub from 'pubnub';

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

server.js:

import PubNub from 'pubnub';

const server = new PubNub({
publishKey: 'YOUR_PUBLISH_KEY',
subscribeKey: 'YOUR_SUBSCRIBE_KEY',
secretKey: 'YOUR_SECRET_KEY',
userId: 'moderation-service',
});

Replace YOUR_PUBLISH_KEY, YOUR_SUBSCRIBE_KEY, and YOUR_SECRET_KEY with the values from your keyset.

The secret key grants privileged access to your PubNub application. It must remain on a trusted server and must never be included in client applications.

Give hide decisions their own channel​

game.chat.moderation carries only one thing: a hide decision naming the timetoken of a message to hide. Nothing publishes chat content there, and no fan's token grants write access to it. moderator.js is the only client in this tutorial authorized to publish on it, and it proves that by presenting a token server.js grants it, bound to moderator-7 specifically, the same authorizedUserId binding Mute and ban a disruptive fan uses to restrict a fan's own token. A fan's own token grants read and write on game.chat, so a fan can publish a chat message or a reaction there, but that grant says nothing about game.chat.moderation. Without a grant naming that channel, a request to publish there is rejected before it reaches any client, regardless of what the fan puts in the message body. That is what makes a hide decision trustworthy: not the shape of the message, but the channel it had to be authorized to reach.

Everyone subscribed to game.chat.moderation can still read every hide decision on it, live and from history, the same as any other channel. Only writing to it is restricted.

Hide (control channel)Delete (Message Persistence)
Reaches connected clientsImmediatelyNot directly. A client that already has the message keeps it.
ReversibleYes, publish a new decisionNo
Message stays in historyYesNo
Needs a moderator-scoped grantYesNo, needs the secret key instead

The moderator reviews a message that game.chat has already delivered, then chooses between two paths. A hide decision on the control channel reaches every connected client, and each one acts on it live. A deletion from Message Persistence removes the message from history but sends nothing to clients, so a client that already has the message keeps showing it.

Flag a message first, so every current fan stops seeing it right away. Delete it only once you've decided the message shouldn't exist in your records at all, since that step can't be undone.

Grant the moderator's token​

Issue moderator.js a token that can publish to the control channel.

1

Run this from server.js and copy the token it prints. authorizedUserId: 'moderator-7' means only a client authenticating as moderator-7 can use it. The token carries write on game.chat.moderation. A fan's own token, granted elsewhere with read and write scoped to game.chat only, carries no such permission, so a fan's client cannot publish a hide decision no matter what it sends.

Apply the printed token in moderator.js:

1

Flag a message as hidden​

Publish a hide decision naming the message you want hidden.

1

Append this call to moderator.js, replacing replace-with-message-timetoken with the timetoken of the message you want hidden, for example one your terminal printed while running Build match-day chat for fans. PubNub rejects this publish outright if moderator.js hasn't applied a token granting write on game.chat.moderation, before your own code ever runs. Keep the message's own timetoken. A later step needs it again.

Hide the message on every client​

Register the handler that turns a hide decision into a hidden message in your UI.

1

Add this to moderator.js as a second subscription, this time on game.chat.moderation. In production, every fan's client runs this same handler and keeps its own set of hidden timetokens, checking it whenever it renders a message from game.chat. That's what makes the message disappear from their view the moment a moderator flags it, with no separate broadcast of your own.

Keep it hidden for fans who load history​

Fetch both channels together, so a fan who joins after you flagged a message still renders it as hidden.

1

Call this from moderator.js to see the shape a late-joining fan's client gets back. This fetches game.chat and game.chat.moderation in the same call, so a fan's client can build the same hidden-timetoken set from history that Hide the message on every client builds live, then apply it while rendering game.chat's messages. A client that only fetched game.chat would never see the hide decisions at all, since those live on the other channel.

Delete the message for good​

Delete the flagged message from Message Persistence permanently, from server.js.

1

Replace the example messageTimetoken with the timetoken of the message you flagged, and keep it as a string. A PubNub timetoken exceeds the largest integer a JavaScript Number represents exactly, so increment or compare timetokens with an arbitrary-precision integer type such as BigInt rather than with plain numeric arithmetic. That's why the code computes start as the timetoken minus 1 with BigInt and passes the original string as end. The call deletes only the flagged message. For how start and end select messages, refer to Delete messages from history.

Run it only from server.js, since the secret key it needs must never reach a client. There's no undo. Once this call succeeds, the message is gone from every future history fetch, on every client.

Let a policy do the routine cases​

Flagging and deleting one message at a time doesn't scale to the volume of obvious violations a busy match produces. Auto Moderation runs before a message publishes, so a policy can block or report the routine cases automatically, leaving moderators to make the judgment calls this tutorial's manual flag handles. Set one up in Configure Auto Moderation.

Beta feature

Auto Moderation is in beta and available upon request. Contact PubNub Support or Sales.

Run it​

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

node server.js

server.js grants the moderator token and logs token that lets moderator-7 publish hide decisions: .... It then tries to delete replace-with-message-timetoken, which isn't a real timetoken yet, so that call logs an error. That's expected on this first run.

Copy the printed token into moderator.js, replacing replace-with-the-token-server-js-printed. Then, using a real message timetoken from Build match-day chat for fans, replace replace-with-message-timetoken in moderator.js. In the second terminal, run:

node moderator.js

moderator.js flags the message, logs message flagged at timetoken: ..., immediately logs the same hide event a fan's client would receive, then logs the history fetch showing game.chat's message rendered as hidden. Now replace replace-with-message-timetoken in server.js with the same real timetoken, and run node server.js again:

node server.js

This time the delete call succeeds and logs message deleted from Message Persistence: .... Press Ctrl+C in either terminal to stop it.

What happened​

  1. server.js granted moderator-7 a token with write on game.chat.moderation, the only permission that lets a client publish a hide decision.
  2. moderator.js applied that token and published a hide decision naming the flagged message's timetoken. A fan's own token, scoped only to game.chat, could not have made that same call.
  3. Every client subscribed to game.chat.moderation, moderator and fan alike, received that decision live and hid the referenced message.
  4. A history fetch across both channels returned the same decision, so a fan who joins later renders the message as hidden too.
  5. server.js, holding the keyset's secret key, deleted the message from Message Persistence. From that point on, no fetch of game.chat's history returns it.

Next steps​

Was this page useful?

Last updated on