← Files InsForgeARCHIVED FILE

skills/insforge/realtime/sdk-integration.md

9.34 KB · Oct 4, 2026 · 12:28 UTC

↓ Download file

# 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 @insforge/cli secrets get ANON_KEY`. See the main [SKILL.md](../SKILL.md) for framework-specific variable names and full setup steps.

```javascript
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:

```typescript
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](../../insforge-cli/references/realtime.md) reference. This SDK guide covers frontend connection, subscription, publishing, and presence handling.

## Usage Examples

### Connect

```javascript
await insforge.realtime.connect()
console.log('Connected:', insforge.realtime.isConnected)
```

### Subscribe to Channel

```javascript
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:

```javascript
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.members` is the initial source of truth for who is already in the channel
- `presenceId` is the stable key for a member: user ID for `type: 'user'`, socket ID for `type: '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

```javascript
// 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

```javascript
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

```javascript
// Must be subscribed to channel first
await insforge.realtime.publish('chat:room-1', 'new_message', {
  text: 'Hello!',
  sender: 'Alice'
})
```

### Unsubscribe and Disconnect

```javascript
insforge.realtime.unsubscribe('order:123')
insforge.realtime.disconnect()
```

### Connection Events

```javascript
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

```javascript
insforge.realtime.isConnected           // boolean
insforge.realtime.connectionState       // 'disconnected' | 'connecting' | 'connected'
insforge.realtime.socketId              // string
insforge.realtime.getSubscribedChannels() // string[]
```

### Message Metadata

All messages include `meta`:

```javascript
const message = {
  meta: {
    messageId: 'uuid',
    channel: 'order:123',
    senderType: 'system' | 'user',
    senderId: 'user-uuid',  // if user
    timestamp: 'ISO string'
  },
  // ...payload fields
}
```

### Complete Example

```javascript
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

1. **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.

2. **Seed presence from `subscribe()` before processing deltas**
   - Treat `response.presence.members` as the initial source of truth
   - Apply `presence:join` and `presence:leave` as incremental updates after the subscribe call succeeds
   - Use `presenceId` as your stable UI key

3. **Handle connection events and rebuild presence after reconnect**
   - Listen for `connect`, `disconnect`, and `connect_error` events
   - Presence is ephemeral and tracked in-memory on a single backend instance, so reconnect by subscribing again and rebuilding from the returned snapshot

4. **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 === false` before running it or flipping a "first event wins" guard
   - See [../auth/sdk-integration.md#gate-user-dependent-side-effects-during-auth-loading](../auth/sdk-integration.md#gate-user-dependent-side-effects-during-auth-loading)

5. **Design for presence visibility rules**
   - Authenticated subscribers expose their user ID through `presenceId` to other channel members
   - Use non-presence channels when subscriber identity should stay opaque

6. **Clean up subscriptions**
   - Unsubscribe from channels when no longer needed
   - Disconnect when leaving the page/component

### Recommended Workflow

```text
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 |

SHA-256: 5fc5cee437c9346b6f5e74a4373e6fa401f33edd2e3afcf14297b6935e5fbc1a