一键导入
volcano-realtime
Detailed guidance for live subscriptions, broadcast, and presence with Volcano Realtime
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Detailed guidance for live subscriptions, broadcast, and presence with Volcano Realtime
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
| name | volcano-realtime |
| description | Detailed guidance for live subscriptions, broadcast, and presence with Volcano Realtime |
Implement live updates with deterministic realtime lifecycle handling. This skill is self-contained: connection lifecycle, all three channel types (postgres / broadcast / presence), token refresh, and cleanup are embedded.
VolcanoRealtime with apiUrl, anonKey, and accessToken (or getToken).connect(), then create channels with the right type.subscribe().subscribe() with unsubscribe() on teardown; pair connect() with disconnect().centrifuge is required.ws only for Node.js server-side realtime usage. The SDK uses the browser's native WebSocket automatically and ws automatically in Node.js — provide a custom implementation only for advanced cases (see "Custom WebSocket Implementation" below).import { VolcanoRealtime } from '@volcano.dev/sdk/realtime';
const realtime = new VolcanoRealtime({
apiUrl: 'https://api.yourproject.volcano.dev',
anonKey: 'your-anon-key',
accessToken: volcano.accessToken, // JWT from auth session
});
await realtime.connect();
Browser WebSocket connections include an Origin header. When CORS is enabled for your project, that origin must be listed in the project's auth CORS allowed origins or the WebSocket upgrade is rejected before authentication completes — connect fails silently from the client's point of view.
For local development, add your app origin (e.g. http://localhost:3000) to the project's auth CORS allowed origins in the Volcano dashboard.
Server-side Node.js connections usually do not send an Origin header and are not blocked by browser CORS checks.
Symptoms of a CORS misconfiguration:
realtime.connect() resolves but onConnect never fires.onError fires with an opaque WebSocket error (no message).Failed or (blocked).Fix: add the origin to the project's auth CORS allowed origins, then reconnect.
Most applications do not need this. The SDK uses the browser's native WebSocket in browsers and ws in Node.js. For Node.js tests or advanced server-side clients that need to inject custom headers (e.g., a fixed Origin header to satisfy CORS in a non-browser context), pass a webSocket constructor:
import WebSocket from 'ws';
class OriginWebSocket extends WebSocket {
constructor(address, protocols, options = {}) {
super(address, protocols, {
...options,
headers: {
...options.headers,
Origin: 'https://app.example.com',
},
});
}
}
const realtime = new VolcanoRealtime({
apiUrl: 'https://api.yourproject.volcano.dev',
anonKey: 'your-anon-key',
accessToken: volcano.accessToken,
webSocket: OriginWebSocket,
});
The webSocket config takes any WebSocket-compatible constructor — useful for tests that mock the transport, or for environments that need TLS client certs / proxy support beyond what ws exposes by default.
realtime.onConnect((ctx) => {
// ctx.client, ctx.latency
});
realtime.onDisconnect((ctx) => {
// ctx.reason, ctx.reconnect (auto-reconnect with exponential backoff)
});
realtime.onError((ctx) => {
// ctx.message
});
const channel = realtime.channel('my-changes', { type: 'postgres' });
The signature is channel.onPostgresChanges(eventType, schema, table, callback). The schema argument is the Postgres schema name — typically 'public' for application tables.
channel.onPostgresChanges('*', 'public', 'posts', (change) => {
// change.type: 'INSERT' | 'UPDATE' | 'DELETE'
// change.table, change.schema, change.timestamp
// INSERT: change.record
// UPDATE: change.record, change.old_record, change.columns
// DELETE: change.old_record
});
await channel.subscribe();
channel.onPostgresChanges('INSERT', 'public', 'messages', (c) => addMessage(c.record));
channel.onPostgresChanges('UPDATE', 'public', 'posts', (c) => updatePost(c.record));
channel.onPostgresChanges('DELETE', 'public', 'posts', (c) => removePost(c.old_record.id));
const channel = realtime.channel('app-changes', { type: 'postgres' });
channel.onPostgresChanges('*', 'public', 'posts', handlePostChange);
channel.onPostgresChanges('*', 'public', 'comments', handleCommentChange);
channel.onPostgresChanges('*', 'public', 'reactions', handleReactionChange);
await channel.subscribe();
Each user only receives events for rows their RLS policy allows them to see. Same channel, different deliveries per user.
Messages aren't persisted; only currently subscribed clients receive them.
const channel = realtime.channel('notifications', { type: 'broadcast' });
channel.on('notification', (data) => {
showNotification(data.title, data.message);
});
channel.on('*', (data, ctx) => {
// catch-all listener
});
await channel.subscribe();
await channel.send({
type: 'notification',
title: 'New Feature!',
message: 'Check out our latest update',
});
const channel = realtime.channel('chat-room-123', { type: 'broadcast' });
channel.on('typing', (data) => {
if (data.user_id !== currentUser.id) showTyping(data.user_id);
});
channel.on('stopped_typing', (data) => hideTyping(data.user_id));
await channel.subscribe();
let typingTimer: any;
function onInput() {
channel.send({ type: 'typing', user_id: currentUser.id });
clearTimeout(typingTimer);
typingTimer = setTimeout(() => {
channel.send({ type: 'stopped_typing', user_id: currentUser.id });
}, 2000);
}
const channel = realtime.channel('lobby', { type: 'presence' });
await channel.subscribe();
await channel.track({
user_id: currentUser.id,
username: currentUser.name,
status: 'online',
avatar: currentUser.avatar_url,
});
channel.onPresenceSync((state) => {
// state: { [clientId]: userData }
const onlineUsers = Object.entries(state).map(([clientId, data]) => ({ clientId, ...data }));
updateOnlineUsersList(onlineUsers);
});
const state = channel.getPresenceState();
for (const [clientId, userData] of Object.entries(state)) {
// ...
}
await channel.track({
user_id: currentUser.id,
username: currentUser.name,
status: 'away',
last_seen: new Date().toISOString(),
});
channel.unsubscribe();
realtime.removeChannel('my-channel', 'postgres');
realtime.removeAllChannels();
realtime.isConnected(); // boolean
realtime.disconnect();
accessToken vs getTokenUse accessToken for short-lived UI sessions where the page lifecycle is bounded by JWT expiry (~1 hour). Use getToken for long-lived clients — background tabs, desktop/native apps, edge workers, server-side polling — so the realtime client refreshes the JWT seamlessly without dropping the connection.
const realtime = new VolcanoRealtime({
apiUrl: 'https://api.example.com',
anonKey: 'anon-key',
getToken: async () => {
const { session } = await volcano.auth.refreshSession();
return session.access_token;
},
});
const volcano = new VolcanoAuth({ ... });
volcano.database('your_database_name');
const realtime = new VolcanoRealtime({
apiUrl: 'https://api.example.com',
anonKey: 'anon-key',
accessToken: volcano.accessToken,
volcanoClient: volcano, // enables auto-fetch lightweight mode
databaseName: 'your_database_name', // optional if database(...) was called
});
import {
VolcanoRealtime,
RealtimeChannel,
PostgresChange,
PresenceState,
ConnectContext,
DisconnectContext,
ErrorContext,
} from '@volcano.dev/sdk/realtime';
// Load initial data
const { data: posts } = await volcano
.from('posts')
.select('*')
.order('created_at', { ascending: false })
.limit(50);
setPosts(posts ?? []);
// Subscribe for updates
channel.onPostgresChanges('INSERT', 'public', 'posts', (c) => {
setPosts((cur) => [c.record, ...cur]);
});
channel.onPostgresChanges('UPDATE', 'public', 'posts', (c) => {
setPosts((cur) => cur.map((p) => (p.id === c.record.id ? c.record : p)));
});
channel.onPostgresChanges('DELETE', 'public', 'posts', (c) => {
setPosts((cur) => cur.filter((p) => p.id !== c.old_record.id));
});
await channel.subscribe();
useEffect(() => {
const realtime = new VolcanoRealtime({ /* ... */ });
realtime.connect();
const channel = realtime.channel('updates', { type: 'postgres' });
channel.onPostgresChanges('*', 'public', 'posts', handleChange);
channel.subscribe();
return () => {
channel.unsubscribe();
realtime.disconnect();
};
}, []);
onConnect so missed events are reconciled.getToken for long-lived sessions instead of a static accessToken.realtime.onError((ctx) => {
showConnectionError(ctx.message);
});
try {
await channel.subscribe();
} catch (error) {
console.error('Subscription failed:', error.message);
}
connect() is paired with disconnect(); subscribe() is paired with unsubscribe().subscribe().centrifuge is present; ws only when Node-side realtime is used.Install or upgrade the Volcano CLI from a plugin-shipped skills environment.
Detailed guidance for authentication flows built with the Volcano SDK
Detailed guidance for browser and function data access with the Volcano query builder
Reusable error handling patterns for Volcano SDK apps: centralized error dispatcher with action enum, useApiCall React hook for loading/error/data state, retry with exponential backoff, retry with toast notifications, and cross-domain error message catalog.
Detailed guidance for server-side function invocation and orchestration with Volcano
Detailed guidance for using the Volcano SDK correctly in Next.js environments