Mute and ban a disruptive fan
This tutorial builds fan behavior control for match-day chat with Access Manager on the PubNub JavaScript SDK. By the end, a token service grants a fan read and write on game.chat, mutes the fan by revoking every writable token it has issued that fan and replacing it with a read-only one, then bans the fan by revoking every token it has issued that fan, period. The fan's own client observes each change through its status listener. Every token in this tutorial is minted server-side with the keyset's secret key, and a client that holds the secret key itself has no restriction worth enforcing.
authorizedUserId binds a token to one User ID, so the token only authorizes requests made as that User ID. That's not the same as making a stolen token harmless. A User ID isn't a secret: it's readable from the token itself, and a client sets its own by configuration, the same way this tutorial's fan.js sets userId: 'fan-42'. Anyone holding a copy of fan-42's token can configure their own client with userId: 'fan-42' and use it exactly as fan-42 could. What actually keeps a fan from acting as someone else is that your server, not the token, decides who receives which token in the first place. The User ID binding only limits what a given token can be used for once issued. Treat every token as a secret: deliver it only over TLS from your authenticated server, keep its TTL as short as the session tolerates, and revoke it the moment a session ends or a fan is removed, rather than relying on the binding alone.
What you'll build
server.js is your token service. It grants fan-42 a token with read and write on game.chat through Access Manager, and you pass that token to fan.js, which subscribes and publishes. To mute the fan, server.js revokes every writable token it issued and grants a read-only one. The fan can still read the chat, but PubNub rejects their next publish. To ban the fan, server.js revokes every token, and fan.js's status listener reports PNAccessDeniedCategory without a restart.
Before you begin
You need:
- Node.js 22 or later.
- 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.
- Access Manager enabled on the keyset. Without it, any client holding your keys already has full access, and there's nothing for a token to restrict.
- The Revoke v3 Token setting turned on in the same ACCESS MANAGER section of your keyset settings. Configure access control is where you turn it on, and revoking a token fails without it.
You also need the publish key, subscribe key, and secret key from that 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.
Set up the project
Create a directory and install the SDK:
mkdir fan-behavior
cd fan-behavior
npm init -y
npm install pubnub
Add "type": "module" to package.json. Create two files: server.js, your token service, which holds the keyset's secret key, and fan.js, the fan's own client, which never sees it.
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',
});
fan.js:
import PubNub from 'pubnub';
const fanClient = new PubNub({
publishKey: 'YOUR_PUBLISH_KEY',
subscribeKey: 'YOUR_SUBSCRIBE_KEY',
userId: 'fan-42',
});
Replace YOUR_PUBLISH_KEY, YOUR_SUBSCRIBE_KEY, and YOUR_SECRET_KEY with the values from your keyset.
server.js runs once per command, from a terminal, as node server.js <command> [userId]. fan.js runs once and keeps running, as node fan.js <token>.
Access Manager checks the token on every request to game.chat, and your token service decides which token each fan holds. A read-and-write token lets the fan subscribe and publish. A read-only token lets the fan subscribe but rejects a publish. PubNub rejects a revoked token outright. The client only carries the token it was given and never decides what it allows.
Track every issued token
A mute or a ban only works if your server knows every token it has already handed that fan, so it can revoke the ones that no longer belong. A single new "current" token isn't enough by itself: a fan who keeps an earlier, wider token can go on using it right through a narrower one you issued afterward, unless you revoke the earlier one too.
1
Add this to server.js. A production token service keeps this same information, which tokens are outstanding for which fan, and whether that fan is active, muted, or banned, in a database, so it survives a restart and scales across more than one server process. This tutorial persists it in a JSON file next to the script instead, so the four commands below share state across separate node server.js invocations without asking you to stand up a database just to follow along.
Let the fan into the chat
Grant the fan read and write access to game.chat from your token service.
1
Add this to server.js. authorizedUserId binds the token to fan-42 specifically, so Access Manager rejects any request made with this token by a client configured with a different userId. This function also checks the fan's tracked status first: a banned fan gets no token at all, and a muted fan gets a read-only one even from this call, so asking for a fresh grant is never a way around a mute still in effect. Every token needs a time to live (TTL), and PubNub enforces a minimum and a maximum on how long one can last. Refer to Access Manager for the exact bounds. This example grants 60 minutes.
Watch for access changes before you connect
Register the fan's status listener before the fan's client subscribes to anything.
1
Add this to fan.js, before the code from Apply the token and start chatting. This listener reports on the fan's subscription, not on individual publish calls. When a revoke reaches the token the fan is subscribed with, the subscription's next request is rejected and arrives here as PNAccessDeniedCategory, naming game.chat in affectedChannels. A rejected publish never arrives here. It rejects the publish call's own promise, which the next step catches. Registering it first means it's already watching by the time the subscription in the next step opens, rather than racing to attach after traffic could already be flowing. Refer to monitoring connection status for how the rest of a status listener works.
Apply the token and start chatting
Parse the token from the command line, apply it, then connect and try to chat.
1
1
Add both of these to fan.js, after the listener from Watch for access changes before you connect. process.argv[2] is whatever you pass after fan.js on the command line, so node fan.js <token> is what actually applies a real token, rather than a hard-coded placeholder. The publish call is wrapped in its own try/catch. A rejected publish shows up right there, as a rejected promise, not on the status listener. The status listener reports what happens to the fan's standing subscription instead, a separate, longer-lived thing from any one publish call. From here, the client's only job regarding this token is to hold it, send it with every call, and ask your server for a new one before it expires. The client never decides what the token allows. It only carries what your server already decided.
Mute the fan
Revoke every writable token this fan currently holds, then issue a narrower one.
1
Add this to server.js. A mute here is not a separate feature. It looks at every token this fan currently holds according to your tracked state, revokes each one that carries write, and only then issues a new token with write left out, so the fan keeps read and keeps seeing the chat. Revoking the old tokens is what actually enforces the mute. Issuing a narrower one on its own would do nothing if the fan simply kept using a wider token you'd already handed out. The mute is recorded before the new token is issued. If a revoke fails, that token stays in the tracked list and the command prints how many writable tokens are still valid, so running mute again retries them. Don't treat a mute as enforced until every writable token is revoked.
Refer to Grant, change, and revoke permissions for what revocation does and does not undo, including that a revoked token can't be re-enabled. That page also notes that revocation can take up to about a minute to take effect, because PubNub caches a token's non-revoked state for that long. Until it does, a request made with the just-revoked token can still succeed. Once it does, that same request fails with a 403. An older writable token keeps working until the revoke reaches it, even if the fan is also holding the new read-only token. When the 15-minute read-only token expires, the fan doesn't get write back. They have no valid token until your server issues another one, and Let the fan into the chat keeps issuing read-only tokens for as long as the fan's tracked status is muted. The same check is what makes refresh safe: a muted fan who asks your token service for a new token gets another read-only one, and a banned fan gets none.
Notice the mute on the fan's client
Restart fan.js with the muted token, then try to publish again.
node fan.js <mutedToken>
The listener from Watch for access changes before you connect is already registered, so no new code is needed here. This time the publish attempt in Apply the token and start chatting is rejected, and your catch block logs it directly. Your UI should treat a rejected publish carrying an access-denied status as a sign to stop letting the fan type, rather than retry the same publish, since retrying a rejected token reproduces the same rejection.
Ban the fan
Revoke every token this fan currently holds, read-only and writable alike.
1
Add this to server.js. This is the same tracked-token list Mute the fan uses, except every entry gets revoked, not only the writable ones, and the fan's tracked status becomes banned, so a later call to Let the fan into the chat refuses to issue this fan anything at all until you decide otherwise. With fan.js still running on the muted token, this revokes it too. Shortly after, without you restarting fan.js, its status listener logs a rejection on the same PNAccessDeniedCategory category, once the revoke propagates.
Bring the fan back
Lift a mute or a ban by granting a fresh token.
1
Add this to server.js. It sets the fan's tracked status back to active and issues a fresh read-and-write token. Calling this on a banned fan is a deliberate decision to let them back in. It doesn't happen as a side effect of anything else in this tutorial.
Wire up the commands
Route the command line to the four functions above.
1
Add this to the end of server.js, after every function it calls. process.argv[2] is the command you typed, grant, mute, unmute, or ban, and process.argv[3], if you passed one, is the User ID to act on. This is what actually reads node server.js grant fan-42 in Run it and calls grantChatAccess('fan-42'), rather than the command line being decorative text next to code that always does the same thing.
Check what a token allows
Decode a token to see exactly which permissions it carries.
1
Run this from fan.js, or from any script holding the token, against any token you printed earlier in this tutorial. parseToken needs no secret key, so support tooling can use it too. This is the step to reach for when a fan can seemingly still do something you thought you'd taken away. It shows the permissions PubNub stored on the token, rather than the permissions you intended to grant it.
Run it
Run each step from server.js in order, applying the printed token to fan.js between steps.
node server.js grant fan-42
Copy the printed token, then run:
node fan.js <token>
fan.js connects and its publish succeeds. Leave it running, or stop it and continue. Next, mute the fan:
node server.js mute fan-42
This revokes the token you just applied. Copy the new, read-only token, stop fan.js, and restart it:
node fan.js <mutedToken>
fan.js connects, and this time its publish attempt logs an access-denied error from its own catch block instead of succeeding. Finally, with fan.js still running on the muted token, ban the fan from a new terminal:
node server.js ban fan-42
Shortly after, fan.js's status listener logs an access-denied status too, without you restarting it, once the revoke propagates. To let the fan back in, run:
node server.js unmute fan-42
and apply the newly printed token the same way. Press Ctrl+C in either terminal to stop it.
What happened
server.jsgrantedfan-42a token withreadandwriteongame.chat, bound to that User ID, and recorded it as an outstanding writable token.fan.jsregistered its status listener, applied the token, subscribed, and published successfully, since the token allowed it.server.jsrevoked that token, then issued a second, narrower one for the same fan, withwriteleft out, and recorded the new token in place of the old.fan.jsapplied the narrower token, and its next publish attempt failed in its owntry/catch, reported directly at the call site.server.jsrevoked every token this fan held, including the muted one still applied infan.js, andfan.js's next request failed too, reported through the same status listener, without a restart.server.jsgranted a fresh read-and-write token, restoring what a ban or mute had removed.
Next steps
- Build match-day chat for fans. Build the chat this tutorial's fan connects to.
- Hide and remove chat messages during a match. Act on one message instead of on a fan's whole access.
- Access Manager. The token model, TTL bounds, and revocation constraints behind every call on this page.
- Permission model. Every permission flag a token can carry, beyond
readandwrite.