WebSocket Subscriptions
The SDK provides real-time event subscriptions via WebSocket. Subscribe to game events for live leaderboards, activity feeds, and instant notifications.
Architecture
Events flow from the blockchain through a multi-layered pipeline:
Blockchain Event (e.g. score update)
↓
Apibara Indexer → writes to PostgreSQL
↓
PostgreSQL LISTEN/NOTIFY triggers
↓
API Server broadcasts to subscribed WebSocket clients
↓
denshokan-sdk WebSocketManager
↓
Event mappers (snake_case → camelCase)
↓
React hooks / callback handlersThe API server uses PostgreSQL triggers on the tokens, games, and minters tables. When data changes, triggers fire NOTIFY on named channels, and the API server broadcasts to all subscribed WebSocket clients.
Channels
| Channel | Event Type | Description |
|---|---|---|
tokens | TokenUpdateEvent | Any token state change (score update, game over, or mint) |
scores | ScoreEvent | When a token's score changes |
game_over | GameOverEvent | When a game session ends |
mints | MintEvent | When new game tokens are minted |
games | NewGameEvent | When a new game is registered |
minters | NewMinterEvent | When a new minter is registered |
settings | NewSettingEvent | When a new game setting is created |
objectives | NewObjectiveEvent | When a new game objective is created |
Event Payloads
Each channel delivers a typed event. The SDK automatically maps snake_case server data to camelCase TypeScript types.
interface ScoreEvent {
tokenId: string;
gameId: number;
score: number;
ownerAddress: string;
playerName: string;
}
interface GameOverEvent {
tokenId: string;
gameId: number;
score: number;
ownerAddress: string;
playerName: string;
completedAllObjectives: boolean;
}
interface MintEvent {
tokenId: string;
gameId: number;
ownerAddress: string;
mintedBy: string;
settingsId: number;
}
// TokenUpdateEvent is polymorphic — it aggregates all token-related events
interface TokenUpdateEvent {
type: "scoreUpdate" | "gameOver" | "minted";
tokenId: string;
gameId: number;
score?: number;
ownerAddress?: string;
}
interface NewGameEvent {
gameId: number;
contractAddress: string;
name: string;
}
interface NewMinterEvent {
minterId: string;
contractAddress: string;
name: string;
blockNumber: string;
}
interface NewSettingEvent {
gameAddress: string;
settingsId: number;
creatorAddress: string;
settingsData: string | null;
}
interface NewObjectiveEvent {
gameAddress: string;
objectiveId: number;
settingsId: number;
creatorAddress: string;
objectiveData: string | null;
}All types are exported from the SDK:
import type {
WSChannel,
WSMessage,
ScoreEvent,
GameOverEvent,
MintEvent,
TokenUpdateEvent,
NewGameEvent,
NewMinterEvent,
NewSettingEvent,
NewObjectiveEvent,
WSChannelPayloadMap,
} from "@provable-games/denshokan-sdk";Using the Client
import { DenshokanClient } from "@provable-games/denshokan-sdk";
const client = new DenshokanClient({ chain: "mainnet" });
// Subscribe to score updates for game ID 1
const unsubscribe = client.subscribe(
{
channels: ["scores", "game_over"],
gameIds: [1],
},
(message) => {
console.log(`Channel: ${message.channel}`);
console.log(`Data:`, message.data);
},
);
// Later: unsubscribe
unsubscribe();
// Cleanup all connections
client.disconnect();Typed React Hooks (Recommended)
The SDK provides eight typed hooks — one per channel. Each returns the last event, a buffered event history, and connection state.
useScoreUpdates
import { useScoreUpdates } from "@provable-games/denshokan-sdk/react";
function LiveScores({ gameId }: { gameId: number }) {
const { lastEvent, events, isConnected } = useScoreUpdates({
gameIds: [gameId],
bufferSize: 100,
});
return (
<div>
<p>Connected: {isConnected ? "Yes" : "No"}</p>
<p>Latest: {lastEvent?.playerName} scored {lastEvent?.score}</p>
<ul>
{events.map((e, i) => (
<li key={i}>{e.playerName}: {e.score}</li>
))}
</ul>
</div>
);
}All Typed Hooks
| Hook | Channel | Payload |
|---|---|---|
useScoreUpdates | scores | ScoreEvent |
useGameOverEvents | game_over | GameOverEvent |
useMintEvents | mints | MintEvent |
useTokenUpdates | tokens | TokenUpdateEvent |
useNewGames | games | NewGameEvent |
useNewMinters | minters | NewMinterEvent |
useNewSettings | settings | NewSettingEvent |
useNewObjectives | objectives | NewObjectiveEvent |
Hook Options
interface UseChannelOptions<C extends WSChannel> {
gameIds?: number[]; // Filter to specific game IDs (server-side)
bufferSize?: number; // Max events to keep in memory (default: 50)
enabled?: boolean; // Enable/disable the subscription (default: true)
onEvent?: (event: WSChannelPayloadMap[C]) => void; // Callback per event
}Hook Return Value
interface UseChannelResult<C extends WSChannel> {
lastEvent: WSChannelPayloadMap[C] | null; // Most recent event
events: WSChannelPayloadMap[C][]; // Buffered event history
isConnected: boolean; // WebSocket connection state
clear: () => void; // Clear event buffer
}useChannelSubscription (Generic)
All typed hooks wrap useChannelSubscription. You can use it directly for type-safe access to any channel:
import { useChannelSubscription } from "@provable-games/denshokan-sdk/react";
const { lastEvent, events, isConnected } = useChannelSubscription("scores", {
gameIds: [1, 2],
bufferSize: 25,
onEvent: (event) => {
console.log("New score:", event.score);
},
});Low-Level Hook
For raw message access, use useSubscription:
import { useSubscription } from "@provable-games/denshokan-sdk/react";
function LiveLeaderboard({ gameId }: { gameId: number }) {
const [scores, setScores] = useState<Map<string, number>>(new Map());
useSubscription(
["scores", "game_over"],
(message) => {
if (message.channel === "scores") {
const data = message.data as ScoreEvent;
setScores(prev => new Map(prev).set(data.tokenId, data.score));
}
},
[gameId],
);
return (
<ul>
{[...scores.entries()]
.sort(([, a], [, b]) => b - a)
.map(([id, score]) => (
<li key={id}>Token {id}: {score}</li>
))}
</ul>
);
}useSubscription Signature
function useSubscription(
channels: WSChannel[],
handler: WSEventHandler, // (message: WSMessage) => void
gameIds?: number[],
): void;The hook manages connection lifecycle automatically:
- Connects when the component mounts
- Re-subscribes if channels or gameIds change
- Disconnects when the component unmounts
Connection Status
Monitor WebSocket connection state in your UI:
import { useConnectionStatus } from "@provable-games/denshokan-sdk/react";
function ConnectionBadge() {
const { isConnected } = useConnectionStatus();
return (
<span style={{ color: isConnected ? "green" : "red" }}>
{isConnected ? "Live" : "Reconnecting..."}
</span>
);
}For programmatic use outside React:
// Check current status
const connected = client.wsConnected;
// Listen for changes
const unsub = client.onWsConnectionChange((connected) => {
console.log("WebSocket:", connected ? "connected" : "disconnected");
});Message Format
interface WSMessage {
channel: string; // "scores", "game_over", "tokens", "mints", "games", "minters", "settings", "objectives"
data: unknown; // Channel-specific payload (use typed hooks for automatic typing)
_timing?: {
serverTs: number; // Server timestamp for latency measurement
};
}Connection Management
The WebSocket manager handles:
- Auto-reconnect with exponential backoff (1s, 2s, 4s, ... capped at 30s)
- Max 10 reconnect attempts before giving up
- Re-subscription on reconnect (all active subscriptions are restored)
- Smart retry — only attempts reconnection if active subscriptions exist
Manual Connection
// Connect manually (usually not needed - subscribe() auto-connects)
client.connect();
// Check connection status
const connected = client.wsConnected;
// Disconnect
client.disconnect();Combining with Data Hooks
A common pattern is to use data hooks for initial load and typed subscription hooks for live updates:
function GameDashboard({ gameAddress }: { gameAddress: string }) {
const { data: tokens, refetch } = useTokens({
gameAddress,
gameOver: false,
limit: 50,
});
// Automatically refetch when a game completes
useGameOverEvents({
onEvent: () => refetch(),
});
// Show live score updates
const { lastEvent: latestScore } = useScoreUpdates();
return (
<div>
{latestScore && (
<p>Latest: {latestScore.playerName} scored {latestScore.score}</p>
)}
<TokenList tokens={tokens?.data ?? []} />
</div>
);
}This gives you:
- Instant initial render from the API
- Automatic list refresh when games complete
- Live score ticker from WebSocket