Skip to content

Plugin Ecosystem

Arnel Robles edited this page Oct 1, 2026 · 1 revision

Plugin: Ecosystem

dopaminejs-plugin-ecosystem 2.0.0. Needs dopaminejs 2.3.0 or newer.

npm install dopaminejs-plugin-ecosystem

Four exports: BattlePassPlugin, LeaderboardPlugin, WebhookIntegration, RewardMiddleware.

Version 2.0.0 changed the webhook format. Coming from 1.x, read Migration first.

Setup

Both plugins listen on the kernel bus, so the reward system must publish there.

import { Game, RewardSystem, DataService } from 'dopaminejs';
import { BattlePassPlugin, LeaderboardPlugin } from 'dopaminejs-plugin-ecosystem';

const game = new Game();
const rewards = new RewardSystem(new DataService(), { events: game.kernel.events });

game.kernel.plugins.use(BattlePassPlugin);
await rewards.init();

Without events (or kernel) in the reward config, the plugins hear nothing.

BattlePassPlugin

Four tiers by total XP.

Tier level XP Reward
1 0 Bronze Badge
5 500 Silver Badge
10 1500 Gold Badge
20 5000 Platinum Badge

When xp_gained puts the total at or past a tier, the plugin emits battlepass:tier-unlocked with the tier object, on the kernel bus and as a CustomEvent on window.

game.kernel.events.on('battlepass:tier-unlocked', (tier) => {
    console.log(tier.level, tier.reward);
});

window.addEventListener('battlepass:tier-unlocked', (event) => {
    console.log(event.detail.reward);
});

Which tiers were announced is kept in memory only. After a reload, every tier at or below the player's XP fires again on the first xp_gained. Store what you granted and ignore repeats.

LeaderboardPlugin

Posts level ups and achievements to your endpoint.

LeaderboardPlugin.configure({
    webhookUrl: 'https://example.com/hooks/dopamine',
    secret: 'shared-secret',
    playerId: () => currentUser.id
});

game.kernel.plugins.use(LeaderboardPlugin);

configure must run before use. Without a webhookUrl the plugin warns and sends nothing.

playerId is a string or a function. Left out, the plugin generates an id and keeps it in localStorage under dopamine_player_id.

Event data
level_up { playerId, level, xp }
achievement { playerId, achievementId, timestamp }

WebhookIntegration

The sender the leaderboard uses. You can use it directly.

import { WebhookIntegration } from 'dopaminejs-plugin-ecosystem';

const webhook = new WebhookIntegration({
    webhookUrl: 'https://example.com/hooks/dopamine',
    secret: 'shared-secret',
    maxQueue: 100
});

await webhook.send('purchase', { sku: 'gem-pack' });

The request is a POST with Content-Type: application/json and this body:

{ "event": "purchase", "data": { "sku": "gem-pack" }, "timestamp": 1790000000000 }
  • Requests go out one at a time, in order.
  • Past maxQueue waiting requests, the oldest is dropped.
  • A failed request is logged. It is not retried.
  • enabled: false turns sending off.

Signature

With a secret, each request carries:

X-Dopamine-Signature: sha256=<hex>

The value is HMAC-SHA256 of the exact request body, keyed with the secret. Verify it against the raw body before parsing.

import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(rawBody, header, secret) {
    const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex');
    const a = Buffer.from(header ?? '');
    const b = Buffer.from(expected);
    return a.length === b.length && timingSafeEqual(a, b);
}

Where crypto.subtle is missing (a page served over plain HTTP, for example) the request is sent unsigned, with one warning.

What the signature proves

A secret in browser code can be read by the player. The signature shows the request came from a copy of your app. It does not show the numbers are true. Treat the payload as a claim, and check anything that matters against your own records. See Security.

RewardMiddleware

A hook list you run yourself. It is not connected to RewardSystem.

import { RewardMiddleware } from 'dopaminejs-plugin-ecosystem';

const middleware = new RewardMiddleware();

middleware.use('beforeXP', (data) => ({ amount: data.amount * 2 }));
middleware.use('afterXP', (data) => analytics.track('xp', data));

const { amount } = await middleware.executeBefore('XP', { amount: 50 });
await rewards.addXP(amount);
await middleware.executeAfter('XP', { amount });

Hook names: beforeXP, afterXP, beforeLevelUp, afterLevelUp, beforeAchievement, afterAchievement.

  • A before hook may return an object. It is merged over the data and passed on.
  • A hook that throws is logged and skipped.
  • An unknown hook name logs a warning.

Singletons

BattlePassPlugin and LeaderboardPlugin are module-level objects. One page, one game. Two kernels using the same plugin object share its state.

Clone this wiki locally