JavaScript SDK
The Volcano Realtime JavaScript SDK: WebSocket connections with automatic reconnection, subscription management, and TypeScript support.
The Volcano Realtime JavaScript SDK provides a simple interface for WebSocket connections with automatic reconnection, subscription management, and TypeScript support.
Installation
npm install @volcano.dev/sdk centrifugeThe SDK requires the centrifuge package as a peer dependency for WebSocket communication.
Quick start
import { VolcanoRealtime } from '@volcano.dev/sdk/realtime';
// Create a realtime instance
// Note: No projectId needed - it's in your anonKey
const realtime = new VolcanoRealtime({
apiUrl: 'https://api.yourapp.com',
anonKey: 'your-anon-key',
accessToken: 'user-access-token'
});
// Connect
await realtime.connect();
// Create a channel - just specify the name
const channel = realtime.channel('my-channel');
// Subscribe to events
channel.on('message', (payload) => {
console.log('Received:', payload);
});
await channel.subscribe();
// Send a message
await channel.send({ event: 'message', text: 'Hello!' });Configuration
interface RealtimeConfig {
// Required
apiUrl: string; // Your Volcano API URL
anonKey: string; // Your project's anon key (or empty for service key)
accessToken: string; // User's access token or service key
// Optional
getToken?: () => Promise<string>; // Function to refresh token
}Frontend (User Token)
const realtime = new VolcanoRealtime({
apiUrl: 'https://api.yourapp.com',
anonKey: anonKey, // Required for user tokens
accessToken: accessToken,
getToken: async () => {
// Called when token needs refresh
const newToken = await refreshAccessToken();
return newToken;
}
});Backend (Service Key)
For server-side applications, use a service role key:
// Service key only - project ID inferred from key
const realtime = new VolcanoRealtime({
apiUrl: 'https://api.yourapp.com',
anonKey: '', // Not required with service key
accessToken: process.env.SERVICE_KEY // sk-...
});
// With anon key for additional validation
const realtime = new VolcanoRealtime({
apiUrl: 'https://api.yourapp.com',
anonKey: process.env.ANON_KEY,
accessToken: process.env.SERVICE_KEY
});Service key notes:
- Project ID is extracted from the service key JWT
- If anon key is provided, project IDs must match
- Service keys bypass RLS for Postgres changes
- Never expose service keys in client-side code
No project ID needed
Unlike some other SDKs, you never specify a projectId. The project is automatically determined from your anon key JWT:
// ✅ Correct - project comes from anonKey
const realtime = new VolcanoRealtime({
apiUrl: 'https://api.yourapp.com',
anonKey: 'your-anon-key',
accessToken: accessToken
});
// ❌ Wrong - don't pass projectId
const realtime = new VolcanoRealtime({
apiUrl: 'https://api.yourapp.com',
projectId: 'proj-123', // Not needed!
anonKey: 'your-anon-key',
accessToken: accessToken
});Connection lifecycle
Connect
// Returns a promise that resolves when connected
await realtime.connect();Connection events
realtime.onConnect((ctx) => {
console.log('Connected to realtime server');
});
realtime.onDisconnect((ctx) => {
console.log('Disconnected from realtime server');
});
realtime.onError((ctx) => {
console.error('Realtime error:', ctx);
});Disconnect
// Gracefully disconnect
realtime.disconnect();Check connection status
if (realtime.isConnected()) {
console.log('Connected');
}Channels
Create a channel
// Broadcast channel (default)
const channel = realtime.channel('channel-name');
// Presence channel
const presence = realtime.channel('lobby', { type: 'presence' });
// Postgres changes channel
const db = realtime.channel('public:messages', { type: 'postgres' });Channel naming
Just use simple, descriptive names. The SDK handles project namespacing automatically:
// Good - simple names
const chat = realtime.channel('chat-room-123');
const notifications = realtime.channel('user-notifications');
const game = realtime.channel('game-session');
// The SDK sends: "broadcast:chat-room-123"
// Server prefixes: "proj-xxx:broadcast:chat-room-123"Subscribe
// Subscribe and wait for confirmation
await channel.subscribe();Unsubscribe
channel.unsubscribe();Remove all channels
realtime.removeAllChannels();Broadcast events
Listen
// Listen for specific event types
channel.on('message', (payload) => {
console.log('Message:', payload);
});
channel.on('typing', (payload) => {
console.log('User is typing:', payload);
});
// Listen for all events
channel.on('*', (payload) => {
console.log('Any event:', payload);
});Send
// Send a message to all subscribers
await channel.send({
event: 'message',
text: 'Hello everyone!',
userId: 'user-123'
});Presence events
Track presence
const presence = realtime.channel('lobby', { type: 'presence' });
await presence.subscribe();
// Start tracking your presence
presence.track({
name: 'Alice',
status: 'online'
});Get presence state
const state = presence.getPresenceState();
// Returns: { 'client-id': { name: 'Alice', status: 'online' }, ... }Listen for presence events
// Full state sync
presence.onPresenceSync((state) => {
console.log('All users:', state);
});
// User joined
presence.on('join', (info) => {
console.log('User joined:', info);
});
// User left
presence.on('leave', (info) => {
console.log('User left:', info);
});Postgres Changes events
Listen for changes
const db = realtime.channel('public:messages', { type: 'postgres' });
// Listen for specific operations
db.onPostgresChanges('INSERT', 'public', 'messages', (payload) => {
console.log('New row:', payload.record);
});
db.onPostgresChanges('UPDATE', 'public', 'messages', (payload) => {
console.log('Updated row:', payload.record);
console.log('Old values:', payload.old_record);
});
db.onPostgresChanges('DELETE', 'public', 'messages', (payload) => {
console.log('Deleted row ID:', payload.old_record?.id);
});
// Listen for all changes
db.onPostgresChanges('*', 'public', 'messages', (payload) => {
console.log('Change type:', payload.type);
});
await db.subscribe();Row-Level Security
Database changes are filtered by RLS policies. Users only receive changes for rows they have access to:
// If your RLS policy is: user_id = auth.uid()
// Users will only receive changes for their own rows
db.onPostgresChanges('*', 'public', 'notes', (payload) => {
// Only changes to THIS user's notes
console.log('My note changed:', payload.record);
});TypeScript support
import { VolcanoRealtime, RealtimeChannel } from '@volcano.dev/sdk/realtime';
const realtime = new VolcanoRealtime({
apiUrl: 'https://api.yourapp.com',
anonKey: anonKey,
accessToken: accessToken
});
// Typed channel
const channel: RealtimeChannel = realtime.channel('chat');React integration
Custom hook
import { useEffect, useState, useCallback } from 'react';
import { VolcanoRealtime } from '@volcano.dev/sdk/realtime';
export function useRealtime(apiUrl: string, anonKey: string, accessToken: string) {
const [realtime, setRealtime] = useState<VolcanoRealtime | null>(null);
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
const rt = new VolcanoRealtime({
apiUrl,
anonKey,
accessToken
});
rt.onConnect(() => setIsConnected(true));
rt.onDisconnect(() => setIsConnected(false));
rt.connect();
setRealtime(rt);
return () => {
rt.disconnect();
};
}, [apiUrl, anonKey, accessToken]);
return { realtime, isConnected };
}Usage
function ChatRoom({ roomId }) {
const { realtime, isConnected } = useRealtime(
process.env.NEXT_PUBLIC_API_URL,
process.env.NEXT_PUBLIC_ANON_KEY,
accessToken
);
const [messages, setMessages] = useState([]);
useEffect(() => {
if (!realtime || !isConnected) return;
const channel = realtime.channel(`chat-${roomId}`);
channel.on('message', (payload) => {
setMessages(prev => [...prev, payload]);
});
channel.subscribe();
return () => {
channel.unsubscribe();
};
}, [realtime, isConnected, roomId]);
const sendMessage = async (text) => {
const channel = realtime?.channel(`chat-${roomId}`);
await channel?.send({ event: 'message', text, userId: currentUser.id });
};
return (
<div>
{!isConnected && <div>Connecting...</div>}
<MessageList messages={messages} />
<MessageInput onSend={sendMessage} />
</div>
);
}Error handling
try {
await realtime.connect();
} catch (error) {
console.error('Connection failed:', error);
}
// Handle errors via callback
realtime.onError((ctx) => {
console.error('Realtime error:', ctx);
});Best practices
- Single instance: Create one
VolcanoRealtimeinstance per application - Token refresh: Use
getTokencallback for automatic token refresh - Clean up: Always unsubscribe from channels when components unmount
- Error handling: Always handle connection errors
- Simple channel names: Use descriptive names without project prefixes