9.3 KiB
Real-time SDK Integration
Use InsForge SDK for WebSocket pub/sub messaging in your frontend application.
Setup
First, ensure your .env file is configured with your InsForge URL and anon key. Get the anon key with npx -y @insforge/cli secrets get ANON_KEY. See the main SKILL.md for framework-specific variable names and full setup steps.
import { createClient } from '@insforge/sdk'
const insforge = createClient({
baseUrl: process.env.NEXT_PUBLIC_INSFORGE_URL, // adjust prefix for your framework
anonKey: process.env.NEXT_PUBLIC_INSFORGE_ANON_KEY // adjust prefix for your framework
})
For Next.js / SSR Client Components, use @insforge/sdk/ssr so the browser WebSocket can authenticate with the access-token cookie:
import { createBrowserClient } from '@insforge/sdk/ssr'
const insforge = createBrowserClient()
Use createBrowserClient() for authenticated browser Realtime connections. It reads insforge_access_token, refreshes through /api/auth/refresh, and applies same-user refreshes with AuthChangeEvent.TOKEN_REFRESHED internally so active Realtime sockets stay connected; the fresh JWT is used on the next handshake.
Backend Setup
If the task needs channel patterns, database triggers, or channel/message RLS, use the insforge-cli skill's realtime reference. This SDK guide covers frontend connection, subscription, publishing, and presence handling.
Usage Examples
Connect
await insforge.realtime.connect()
console.log('Connected:', insforge.realtime.isConnected)
Subscribe to Channel
const response = await insforge.realtime.subscribe('order:123')
if (!response.ok) {
console.error('Failed:', response.error?.message)
} else {
console.log('Subscribed to:', response.channel)
console.log('Members already present:', response.presence.members)
}
// Auto-connects if not connected
Presence Snapshot on Subscribe
A successful subscribe() always returns a presence snapshot:
const subscribeResponse = {
ok: true,
channel: 'order:123',
presence: {
members: [
{
type: 'user',
presenceId: 'user-123',
joinedAt: '2026-04-25T17:00:00.000Z'
}
]
}
}
Use this snapshot to seed local participant state before listening for live deltas.
presence.membersis the initial source of truth for who is already in the channelpresenceIdis the stable key for a member: user ID fortype: 'user', socket ID fortype: 'anonymous'- Authenticated users are deduplicated into one logical member across multiple sockets or tabs
- Anonymous connections are tracked per socket, so multiple tabs show up as separate members
- Initialize the current user's presence from the
subscribe()response; your own presence is already represented there
Listen for Events
// Listen for events
insforge.realtime.on('status_changed', (payload) => {
console.log('Status:', payload.status)
console.log('Meta:', payload.meta.messageId, payload.meta.timestamp)
})
// Presence deltas for other members in the channel
insforge.realtime.on('presence:join', (message) => {
console.log('Member joined:', message.member.presenceId, message.member.type)
})
insforge.realtime.on('presence:leave', (message) => {
console.log('Member left:', message.member.presenceId)
})
// Listen once
insforge.realtime.once('order_completed', (payload) => {
console.log('Completed:', payload)
})
// Remove listener
insforge.realtime.off('status_changed', handler)
Integrate Presence into UI State
const channel = `chat:${roomId}`
const response = await insforge.realtime.subscribe(channel)
if (!response.ok) throw new Error(response.error?.message || 'Subscribe failed')
let members = response.presence.members
renderMembers(members)
const handleJoin = ({ member, meta }) => {
if (meta.channel !== channel) return
const exists = members.some((current) => current.presenceId === member.presenceId)
members = exists ? members : [...members, member]
renderMembers(members)
}
const handleLeave = ({ member, meta }) => {
if (meta.channel !== channel) return
members = members.filter((current) => current.presenceId !== member.presenceId)
renderMembers(members)
}
insforge.realtime.on('presence:join', handleJoin)
insforge.realtime.on('presence:leave', handleLeave)
Publish Messages
// Must be subscribed to channel first
await insforge.realtime.publish('chat:room-1', 'new_message', {
text: 'Hello!',
sender: 'Alice'
})
Unsubscribe and Disconnect
insforge.realtime.unsubscribe('order:123')
insforge.realtime.disconnect()
Connection Events
insforge.realtime.on('connect', () => console.log('Connected'))
insforge.realtime.on('disconnect', (reason) => console.log('Disconnected:', reason))
insforge.realtime.on('connect_error', (err) => console.error('Error:', err))
insforge.realtime.on('error', ({ code, message }) => console.error(code, message))
Error codes: UNAUTHORIZED, NOT_SUBSCRIBED, INTERNAL_ERROR
Properties
insforge.realtime.isConnected // boolean
insforge.realtime.connectionState // 'disconnected' | 'connecting' | 'connected'
insforge.realtime.socketId // string
insforge.realtime.getSubscribedChannels() // string[]
Message Metadata
All messages include meta:
const message = {
meta: {
messageId: 'uuid',
channel: 'order:123',
senderType: 'system' | 'user',
senderId: 'user-uuid', // if user
timestamp: 'ISO string'
},
// ...payload fields
}
Complete Example
await insforge.realtime.connect()
const channel = `order:${orderId}`
const response = await insforge.realtime.subscribe(channel)
if (!response.ok) throw new Error(response.error?.message || 'Subscribe failed')
let members = response.presence.members
renderPresence(members)
insforge.realtime.on('status_changed', (payload) => {
updateUI(payload.status)
})
insforge.realtime.on('presence:join', ({ member, meta }) => {
if (meta.channel !== channel) return
const exists = members.some((current) => current.presenceId === member.presenceId)
members = exists ? members : [...members, member]
renderPresence(members)
})
insforge.realtime.on('presence:leave', ({ member, meta }) => {
if (meta.channel !== channel) return
members = members.filter((current) => current.presenceId !== member.presenceId)
renderPresence(members)
})
// Client can also publish
await insforge.realtime.publish(channel, 'viewed', {
viewedAt: new Date().toISOString()
})
Best Practices
-
Ensure channel pattern exists before subscribing
- The frontend can only subscribe to channel names that match an enabled backend channel pattern.
- If no channel pattern exists, finish backend setup with the insforge-cli skill first.
-
Seed presence from
subscribe()before processing deltas- Treat
response.presence.membersas the initial source of truth - Apply
presence:joinandpresence:leaveas incremental updates after the subscribe call succeeds - Use
presenceIdas your stable UI key
- Treat
-
Handle connection events and rebuild presence after reconnect
- Listen for
connect,disconnect, andconnect_errorevents - Presence is ephemeral and tracked in-memory on a single backend instance, so reconnect by subscribing again and rebuilding from the returned snapshot
- Listen for
-
Gate user-dependent side effects on auth hydration
- Webhook-backed events can arrive before a cold-load
getCurrentUser()refresh finishes - If an event branches on the current user, wait for
authLoading === falsebefore running it or flipping a "first event wins" guard - See ../auth/sdk-integration.md#gate-user-dependent-side-effects-during-auth-loading
- Webhook-backed events can arrive before a cold-load
-
Design for presence visibility rules
- Authenticated subscribers expose their user ID through
presenceIdto other channel members - Use non-presence channels when subscriber identity should stay opaque
- Authenticated subscribers expose their user ID through
-
Clean up subscriptions
- Unsubscribe from channels when no longer needed
- Disconnect when leaving the page/component
Recommended Workflow
1. Confirm backend setup → channel pattern exists and is enabled
2. Connect to realtime → await insforge.realtime.connect()
3. Subscribe and seed presence → const response = await insforge.realtime.subscribe('channel')
4. Listen for events and deltas → on('event', handler) + on('presence:join'/'presence:leave')
5. Clean up on unmount → unsubscribe() and disconnect()
Common Mistakes
| Mistake | Fix |
|---|---|
| Subscribing before backend channel setup exists | Configure channel patterns, database triggers, and RLS before subscribing |
Waiting for a self presence:join event |
Initialize local presence state from subscribe() response |
| Treating presence as durable/global state | Treat presence as single-instance, in-memory state and resubscribe after reconnects |
| Missing connection error handling | Listen for connect_error and disconnect events |
| Leaving subscriptions active after unmount | Unsubscribe on component unmount |
| Publishing before subscribing | Subscribe to the channel before publishing |