← VolcanoCONTENT HISTORY
WHAT CHANGED
Update to Volcano
Snapshot Oct 24, 2026 · 13:36 UTC · version 0.2.26
Collection source: downloaded plugin package.
First saved snapshot
No earlier snapshot is available to establish a change.
Save Volcano and check future changes in Following.
Up to 10 plugins free · no card required.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"description": "Use for realtime or live updates in Volcano apps, including collaborative boards, chat, presence and online users, live polls and results, leaderboards, Postgres changes, broadcast, WebSockets, and connection lifecycle.",
"included_files": [],
"name": "volcano-realtime",
"skill_md_contents": "---\nname: volcano-realtime\ndescription: Use for realtime or live updates in Volcano apps, including collaborative boards, chat, presence and online users, live polls and results, leaderboards, Postgres changes, broadcast, WebSockets, and connection lifecycle.\n---\n# Volcano Realtime Skill\n\n## Role\nImplement 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.\n\n## Workflow\n1. Enable realtime for the project — `realtime: { enabled: true }` in `volcano-config.yaml`, then `config deploy` (off by default; see \"Enable realtime\").\n2. Initialize `VolcanoRealtime` with `apiUrl`, `anonKey`, and `accessToken` (or `getToken`).\n3. `connect()`, then create channels with the right `type`.\n4. Register handlers BEFORE calling `subscribe()`.\n5. Always pair `subscribe()` with `unsubscribe()` on teardown; pair `connect()` with `disconnect()`.\n6. Subscriptions respect RLS — channel scope is minimal (specific table/event).\n\n## Dependencies\n- `centrifuge` is required.\n- `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).\n\n## Enable realtime (required)\nRealtime is **disabled by default for every project — local and cloud alike**\n(`enabled: false`). Until you enable it, every connection is rejected with\n`realtime disabled for project`. Declare it in `volcano-config.yaml` and deploy:\n```yaml\nrealtime:\n enabled: true # required — off by default\n # broadcast_enabled: true # optional, default true\n # presence_enabled: true # optional, default true\n # postgres_changes_enabled: true # optional, default true\n```\nApply with `volcano config deploy` (local) / `volcano cloud config deploy`\n(cloud). This is the **only** realtime setup that isn't app code — the same\nconfig governs both environments, so a realtime app verified locally deploys to\nthe cloud unchanged. Everything else (`apiUrl`/`anonKey`/`accessToken`, channels,\nhandlers) is identical across environments. In local mode, `apiUrl` is\n`http://localhost:8000` with the local anon key from `volcano status`, and you\ncan sign in as the shipped default user (`clearwater@volcano.dev`) to get an\n`accessToken` without building a signup flow first.\n\n## Initialization\n```ts\nimport { VolcanoRealtime } from '@volcano.dev/sdk/realtime';\n\nconst realtime = new VolcanoRealtime({\n apiUrl: 'https://api.yourproject.volcano.dev',\n anonKey: 'your-anon-key',\n accessToken: volcano.accessToken, // JWT from auth session\n});\n\nawait realtime.connect();\n```\n\n## Browser Origins and CORS\n\nBrowser 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.\n\nFor local development, add your app origin (e.g. `http://localhost:3000`) to the project's auth CORS allowed origins in the Volcano dashboard.\n\nServer-side Node.js connections usually do not send an `Origin` header and are not blocked by browser CORS checks.\n\n**Symptoms of a CORS misconfiguration:**\n- `realtime.connect()` resolves but `onConnect` never fires.\n- `onError` fires with an opaque WebSocket error (no message).\n- Browser DevTools shows the WebSocket request with status `Failed` or `(blocked)`.\n- Same setup works on production but not on localhost (or vice versa).\n\n**Fix:** add the origin to the project's auth CORS allowed origins, then reconnect.\n\n## Custom WebSocket Implementation\n\nMost 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:\n\n```js\nimport WebSocket from 'ws';\n\nclass OriginWebSocket extends WebSocket {\n constructor(address, protocols, options = {}) {\n super(address, protocols, {\n ...options,\n headers: {\n ...options.headers,\n Origin: 'https://app.example.com',\n },\n });\n }\n}\n\nconst realtime = new VolcanoRealtime({\n apiUrl: 'https://api.yourproject.volcano.dev',\n anonKey: 'your-anon-key',\n accessToken: volcano.accessToken,\n webSocket: OriginWebSocket,\n});\n```\n\nThe `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.\n\n## Connection Events\n```ts\nrealtime.onConnect((ctx) => {\n // ctx.client, ctx.latency\n});\nrealtime.onDisconnect((ctx) => {\n // ctx.reason, ctx.reconnect (auto-reconnect with exponential backoff)\n});\nrealtime.onError((ctx) => {\n // ctx.message\n});\n```\n\n## Postgres Changes — live DB events\n\n### Setup\n```ts\nconst channel = realtime.channel('my-changes', { type: 'postgres' });\n```\n\nThe signature is `channel.onPostgresChanges(eventType, schema, table, callback)`. The `schema` argument is the **Postgres schema name** — typically `'public'` for application tables.\n\n### Listen for ALL events on a table\n```ts\nchannel.onPostgresChanges('*', 'public', 'posts', (change) => {\n // change.type: 'INSERT' | 'UPDATE' | 'DELETE'\n // change.table, change.schema, change.timestamp\n // INSERT: change.record\n // UPDATE: change.record, change.old_record, change.columns\n // DELETE: change.old_record\n});\nawait channel.subscribe();\n```\n\n### Filter by event type\n```ts\nchannel.onPostgresChanges('INSERT', 'public', 'messages', (c) => addMessage(c.record));\nchannel.onPostgresChanges('UPDATE', 'public', 'posts', (c) => updatePost(c.record));\nchannel.onPostgresChanges('DELETE', 'public', 'posts', (c) => removePost(c.old_record.id));\n```\n\n### Multiple tables on one channel\n```ts\nconst channel = realtime.channel('app-changes', { type: 'postgres' });\nchannel.onPostgresChanges('*', 'public', 'posts', handlePostChange);\nchannel.onPostgresChanges('*', 'public', 'comments', handleCommentChange);\nchannel.onPostgresChanges('*', 'public', 'reactions', handleReactionChange);\nawait channel.subscribe();\n```\n\n### RLS interaction\nEach user only receives events for rows their RLS policy allows them to see. Same channel, different deliveries per user.\n\n## Broadcast — ephemeral pub/sub\nMessages aren't persisted; only currently subscribed clients receive them.\n\n### Setup\n```ts\nconst channel = realtime.channel('notifications', { type: 'broadcast' });\n```\n\n### Send and receive\n```ts\nchannel.on('notification', (data) => {\n showNotification(data.title, data.message);\n});\nchannel.on('*', (data, ctx) => {\n // catch-all listener\n});\nawait channel.subscribe();\n\nawait channel.send({\n type: 'notification',\n title: 'New Feature!',\n message: 'Check out our latest update',\n});\n```\n\n### Use cases\n- Typing indicators\n- Cursor positions in collaborative editors\n- System-wide notifications\n- Game-state synchronization\n\n### Typing indicator pattern\n```ts\nconst channel = realtime.channel('chat-room-123', { type: 'broadcast' });\n\nchannel.on('typing', (data) => {\n if (data.user_id !== currentUser.id) showTyping(data.user_id);\n});\nchannel.on('stopped_typing', (data) => hideTyping(data.user_id));\nawait channel.subscribe();\n\nlet typingTimer: any;\nfunction onInput() {\n channel.send({ type: 'typing', user_id: currentUser.id });\n clearTimeout(typingTimer);\n typingTimer = setTimeout(() => {\n channel.send({ type: 'stopped_typing', user_id: currentUser.id });\n }, 2000);\n}\n```\n\n## Presence — who's online and their state\n\n### Setup\n```ts\nconst channel = realtime.channel('lobby', { type: 'presence' });\n```\n\n### Track and listen\n```ts\nawait channel.subscribe();\n\nawait channel.track({\n user_id: currentUser.id,\n username: currentUser.name,\n status: 'online',\n avatar: currentUser.avatar_url,\n});\n\nchannel.onPresenceSync((state) => {\n // state: { [clientId]: userData }\n const onlineUsers = Object.entries(state).map(([clientId, data]) => ({ clientId, ...data }));\n updateOnlineUsersList(onlineUsers);\n});\n```\n\n### Read state at any time\n```ts\nconst state = channel.getPresenceState();\nfor (const [clientId, userData] of Object.entries(state)) {\n // ...\n}\n```\n\n### Update state\n```ts\nawait channel.track({\n user_id: currentUser.id,\n username: currentUser.name,\n status: 'away',\n last_seen: new Date().toISOString(),\n});\n```\n\n## Channel Management\n```ts\nchannel.unsubscribe();\nrealtime.removeChannel('my-channel', 'postgres');\nrealtime.removeAllChannels();\nrealtime.isConnected(); // boolean\nrealtime.disconnect();\n```\n\n## Dynamic Token Refresh — `accessToken` vs `getToken`\n\nUse **`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.\n```ts\nconst realtime = new VolcanoRealtime({\n apiUrl: 'https://api.example.com',\n anonKey: 'anon-key',\n getToken: async () => {\n const { session } = await volcano.auth.refreshSession();\n return session.access_token;\n },\n});\n```\n\n## Auto-Fetch Integration with VolcanoAuth\n```ts\nconst volcano = new VolcanoAuth({ ... });\nvolcano.database('your_database_name');\n\nconst realtime = new VolcanoRealtime({\n apiUrl: 'https://api.example.com',\n anonKey: 'anon-key',\n accessToken: volcano.accessToken,\n volcanoClient: volcano, // enables auto-fetch lightweight mode\n databaseName: 'your_database_name', // optional if database(...) was called\n});\n```\n\n## TypeScript\n```ts\nimport {\n VolcanoRealtime,\n RealtimeChannel,\n PostgresChange,\n PresenceState,\n ConnectContext,\n DisconnectContext,\n ErrorContext,\n} from '@volcano.dev/sdk/realtime';\n```\n\n## Initial Fetch + Subscribe Pattern\n```ts\n// Load initial data\nconst { data: posts } = await volcano\n .from('posts')\n .select('*')\n .order('created_at', { ascending: false })\n .limit(50);\nsetPosts(posts ?? []);\n\n// Subscribe for updates\nchannel.onPostgresChanges('INSERT', 'public', 'posts', (c) => {\n setPosts((cur) => [c.record, ...cur]);\n});\nchannel.onPostgresChanges('UPDATE', 'public', 'posts', (c) => {\n setPosts((cur) => cur.map((p) => (p.id === c.record.id ? c.record : p)));\n});\nchannel.onPostgresChanges('DELETE', 'public', 'posts', (c) => {\n setPosts((cur) => cur.filter((p) => p.id !== c.old_record.id));\n});\nawait channel.subscribe();\n```\n\n## React Cleanup Pattern\n```tsx\nuseEffect(() => {\n const realtime = new VolcanoRealtime({ /* ... */ });\n realtime.connect();\n const channel = realtime.channel('updates', { type: 'postgres' });\n channel.onPostgresChanges('*', 'public', 'posts', handleChange);\n channel.subscribe();\n\n return () => {\n channel.unsubscribe();\n realtime.disconnect();\n };\n}, []);\n```\n\n## Best Practices\n- **Throttle presence updates** (e.g., 1 Hz) to avoid flooding the channel.\n- **Refresh data on reconnect** — call your initial-fetch routine inside `onConnect` so missed events are reconciled.\n- **Scope channels** to specific tables/events; broad subscriptions hurt RLS clarity and bandwidth.\n- **Combine initial fetch with subscriptions** — the user sees current state immediately and live updates apply on top.\n- **Use `getToken`** for long-lived sessions instead of a static `accessToken`.\n\n## Error Handling\n```ts\nrealtime.onError((ctx) => {\n showConnectionError(ctx.message);\n});\n\ntry {\n await channel.subscribe();\n} catch (error) {\n console.error('Subscription failed:', error.message);\n}\n```\n\n**Connection rejected (connect times out or never accepts):**\n- `realtime disabled for project` — realtime is off for this project; add\n `realtime: { enabled: true }` to `volcano-config.yaml` and `config deploy`\n (see \"Enable realtime\"). Off by default in local **and** cloud.\n- `invalid apikey` / `anon key not found` — wrong `anonKey` for the target\n project (or the wrong `apiUrl`).\n- Missing/invalid token — realtime requires an authenticated session; pass a\n real `accessToken` (`signIn` first). There is no anonymous/token-less\n realtime connection, in local or cloud.\n\n## Verification Checklist\n- Realtime is enabled for the project (`realtime: { enabled: true }` in `volcano-config.yaml`, `config deploy`d) — off by default.\n- `connect()` is paired with `disconnect()`; `subscribe()` is paired with `unsubscribe()`.\n- Handlers are registered before `subscribe()`.\n- Realtime behavior matches RLS expectations (per-user delivery).\n- Presence updates are throttled when bound to high-frequency events.\n- Dependencies: `centrifuge` is present; `ws` only when Node-side realtime is used.\n\n## Optional Fallback Reference\n"
}SHA-256 of public snapshot: 05868d78dbcf51fb19a28deedebc4a5f65205195c9f0a445b2dc245e6e234055