Show an ad when fan reactions spike
PubNub Illuminate is a real-time decision engine. It evaluates metrics computed from your published messages, and publishes its own message when a condition matches, to a channel your client already subscribes to. In this tutorial, fan reactions published to game.stream-reactions feed an Illuminate metric. One Decision publishes an ad instruction to game.ad-decisions when that metric spikes, a second publishes a rendering change to game.reaction-upgrades, and your client acts on both. Illuminate is a paid add-on, and you can't complete the Decision steps on this page without it active on your account.
What you'll build
fan.js publishes each reaction to game.stream-reactions, where an Illuminate metric counts them. Each Decision watches that metric and fires when the count crosses its own threshold. One publishes an ad instruction to game.ad-decisions, and the other publishes a reaction replacement to game.reaction-upgrades. fan.js subscribes to both channels and only renders what they say, so you change when an ad appears in the Decision, not in the client. Without Illuminate, send-test-decision.js publishes the same messages by hand.
Before you begin
Make sure you have:
- Node.js 22 or later installed.
- A PubNub account and your own keyset. If you don't have one, follow Set up your account to create one.
- Illuminate available on your account. Illuminate runs on a two-week free trial. Continuing to use it after the trial requires a paid plan. See Availability and access.
You can complete the reaction-publishing and client code on this page without Illuminate active. The Business Object, metric, and Decision steps need it.
Set up the project
Create a new directory and install the PubNub SDK:
mkdir sme-real-time-ads
cd sme-real-time-ads
npm init -y
npm install pubnub
Add "type": "module" to package.json, since this tutorial's code uses import.
Create two files: fan.js, which publishes reactions and receives both Decisions, and send-test-decision.js, a small script that publishes the shapes a Decision would publish. send-test-decision.js stands in for Illuminate while you build and test fan.js.
Add this to fan.js:
import PubNub from 'pubnub';
const pubnub = new PubNub({
publishKey: 'YOUR_PUBLISH_KEY',
subscribeKey: 'YOUR_SUBSCRIBE_KEY',
userId: 'fan-42',
});
Add the same initialization to send-test-decision.js, with userId: 'match-service' instead, since this script represents your own infrastructure rather than one fan's client. Replace YOUR_PUBLISH_KEY and YOUR_SUBSCRIBE_KEY in both files with the keys from your keyset.
Publish what fans are doing
Add this to fan.js:
1
A reaction like this is also a good candidate for a signal instead of a message, since you never need to look one up later. This tutorial keeps it a message because Illuminate reads it the same way either source publishes it.
Configure the reaction-count metric in Illuminate
An Illuminate Business Object reads reactions published to game.stream-reactions. New Business Objects pre-map Channel and Message Type. Add a Reaction field with the String type before activation.
| Field | Source | Mapped automatically? |
|---|---|---|
| Channel | $.message.channel | Yes, pre-mapped |
| Message Type | $.message.body.type | Yes, pre-mapped |
| Reaction | $.message.body.reaction | No, add and map it |
Use a metric with Function set to Count, Dimension set to Reaction, and Period set to 1 minute. Filter for Channel Equals game.stream-reactions and Message Type Equals reaction. The dimension keeps each reaction's count separate.
Activate the Business Object after mapping Reaction. You cannot add data fields after activation. For the console steps, see Create a Business Object and Metrics.
Decide to show an ad
Create a Decision on this Business Object, using the reaction-count metric as its condition source. Add a Send Message action targeting Channel ID game.ad-decisions, with a Body such as:
{
"adId": 501,
"clickPoints": 25
}
adId and clickPoints are values your own ad system defines, not PubNub's. This example picks 501 and 25 to stand in for whatever your ad inventory actually uses.
Set the rule's condition on the Count of Reactions field. How high a count counts as a spike is your call, not a fixed product threshold. This example sets it at Count of Reactions greater than or equal to 50 within the metric's one-minute window. Watch how your own audience reacts and adjust from there.
Create a Decision walks through adding the action, the Body, and the rule in the console.
Act on the decision in your client
Add this to fan.js:
1
This subscribes to game.ad-decisions and renders whatever it receives. A message with an adId shows that ad. A message without one, such as { "adId": null, "clickPoints": null }, clears it, since the check is decision.adId, and a falsy value takes the "no ad" branch.
Because of that, this client's whole job is to render what the Decision says, nothing more. Change the threshold, the ad, or when it appears at all, and you change it in the Decision's rule, not in fan.js. If you deactivate the Decision without it ever sending a clearing message first, the last ad fan.js showed stays on screen, since nothing told it otherwise.
Change how a reaction renders
Create a second Decision on the same Business Object and metric. Add a Send Message action targeting Channel ID game.reaction-upgrades, with a Body such as:
{
"reaction": "${Reaction}",
"replacement": "😎"
}
${Reaction} is a condition variable from the metric's Dimension, so it fills in with whichever reaction actually crossed the threshold. replacement is a static value you set per rule, one rule for every reaction you want to upgrade.
Add this to fan.js:
1
This subscribes to game.reaction-upgrades and logs the mapping it receives: which reaction to replace, and what to render instead. Configure the Send Message action's Channel ID to game.reaction-upgrades, so the Decision and this subscription agree on where the instruction travels.
Run it
Add this to send-test-decision.js:
await pubnub.publish({
channel: 'game.ad-decisions',
message: { adId: 501, clickPoints: 25 },
});
In one terminal, start the client:
node fan.js
In a second terminal, run the stand-in:
node send-test-decision.js
fan.js should print:
show ad 501, worth 25 points
Change the message in send-test-decision.js to { adId: null, clickPoints: null } and run it again. This time fan.js prints:
no ad to show, so clear the ad slot
That's the client half of the first Decision, tested without Illuminate ever firing one. The same approach tests the second: publish { reaction: '🔥', replacement: '😎' } to game.reaction-upgrades from send-test-decision.js, and fan.js logs the render instruction the same way it would from a real Decision.
If Illuminate is active on your account, leave fan.js running. Tap the reaction button on your own stream enough times to cross the threshold you set in Decide to show an ad. The same message arrives from the Decision instead of from send-test-decision.js. fan.js keeps running because its subscriptions hold the connection open. Press Ctrl+C to stop it.
What happened
fan.jspublished a reaction togame.stream-reactions, the same message Illuminate's Business Object reads.send-test-decision.jspublished directly togame.ad-decisions, standing in for the Send Message action your first Decision publishes once it's active.fan.js's subscription togame.ad-decisionsreceived the message and rendered the ad, or cleared it, based only on whetheradIdwas present.- The same pattern applies to
game.reaction-upgrades: once your second Decision is active, a burst of one reaction publishes a replacement instruction there, andfan.js's subscription renders it. - Once both Decisions are active, real fan taps replace
send-test-decision.js, and the rest of this flow runs unchanged.
Next steps
- Automated polling. The same Business Object and reaction-count metric, used to trigger a poll instead of an ad.
- Illuminate. How Business Objects, Decisions, and Dashboards fit together.
- Create a Dashboard. Chart the reaction-count metric and both Decisions' triggered actions together.
- Decisions. Limit how often a Decision's action fires while a reaction keeps spiking.