---
title: Realtime SDK Reference
description: Subscribe to channels, publish events, and track presence with the InsForge TypeScript SDK.
---

import Installation from '/snippets/sdk-installation.mdx';

## Installation

<Installation />

## Mental Model

The TypeScript SDK opens one Socket.IO connection to your InsForge backend. You subscribe to named channels, listen for event names, and optionally publish events back to channels you have joined.

Events can come from two places:

- Database triggers that call `realtime.publish(channel, event, payload)`.
- Clients that call `insforge.realtime.publish(channel, event, payload)`.

For the backend channel and RLS model, see [Realtime overview](/core-concepts/realtime/overview).

## Quick Start

```typescript
import { createClient } from '@insforge/sdk';

const insforge = createClient({
  baseUrl: 'https://your-app.insforge.app',
  anonKey: 'your-anon-key'
});

insforge.realtime.on('connect', () => {
  console.log('Connected:', insforge.realtime.socketId);
});

insforge.realtime.on('error', (error) => {
  console.error(error.code, error.message);
});

await insforge.realtime.connect();

const subscription = await insforge.realtime.subscribe('order:123');

if (!subscription.ok) {
  throw new Error(subscription.error.message);
}

insforge.realtime.on('status_changed', (message) => {
  console.log(message.status);
  console.log(message.meta.messageId);
});
```

<Tip>
  Register `connect`, `disconnect`, `connect_error`, and `error` handlers before calling `connect()` so early connection failures are visible.
</Tip>

## connect()

Establish a WebSocket connection.

```typescript
await insforge.realtime.connect();
```

Returns:

```typescript
Promise<void>
```

Notes:

- The SDK includes the current auth token when one exists. If there is no signed-in user, it can use the configured anon key.
- Multiple `connect()` calls while a connection is already in progress reuse the same connection promise.
- The connection attempt times out after 10 seconds.

## subscribe()

Subscribe to a channel and receive the current presence snapshot.

```typescript
const response = await insforge.realtime.subscribe('chat:room-1');

if (response.ok) {
  console.log(response.channel);
  console.log(response.presence.members);
} else {
  console.error(response.error.code, response.error.message);
}
```

Parameters:

| Parameter | Type | Description |
|-----------|------|-------------|
| `channel` | `string` | Resolved channel name, such as `orders`, `order:123`, or `chat:room-1`. |

Returns:

```typescript
type SubscribeResponse =
  | {
      ok: true;
      channel: string;
      presence: {
        members: PresenceMember[];
      };
    }
  | {
      ok: false;
      channel: string;
      error: {
        code: string;
        message: string;
      };
    };
```

`subscribe()` auto-connects if needed. Calling `connect()` explicitly is still recommended so connection event handlers are already attached.

## publish()

Publish an event to a channel.

```typescript
await insforge.realtime.publish('chat:room-1', 'new_message', {
  text: 'Hello',
  sentAt: new Date().toISOString()
});
```

Parameters:

| Parameter | Type | Description |
|-----------|------|-------------|
| `channel` | `string` | Channel to publish to. The client must already be subscribed. |
| `event` | `string` | Event name that subscribers listen for. |
| `payload` | `Record<string, unknown>` | JSON-serializable message payload. |

<Warning>
  Publishing requires a prior successful subscription to the same channel. If RLS is enabled on `realtime.messages`, publish is also checked against `INSERT` policies.
</Warning>

Publish failures are emitted through the `error` event.

## on()

Listen for custom events, connection events, presence events, and realtime errors.

```typescript
insforge.realtime.on('new_message', (message) => {
  console.log(message.text);
  console.log(message.meta.senderType);
});
```

Reserved events:

| Event | Payload | Description |
|-------|---------|-------------|
| `connect` | None | The WebSocket connected. |
| `connect_error` | `Error` | Initial connection or reconnect failed. |
| `disconnect` | `string` | The WebSocket disconnected. |
| `error` | `RealtimeErrorPayload` | Subscribe or publish failed. |
| `presence:join` | `PresenceJoinMessage` | A logical member became present in a channel. |
| `presence:leave` | `PresenceLeaveMessage` | A logical member is no longer present in a channel. |

## once()

Listen for an event once, then remove the listener automatically.

```typescript
insforge.realtime.once('checkout_completed', (message) => {
  console.log('Completed:', message.orderId);
});
```

## off()

Remove an event listener.

```typescript
function handleStatus(message: OrderStatusMessage) {
  console.log(message.status);
}

insforge.realtime.on('status_changed', handleStatus);
insforge.realtime.off('status_changed', handleStatus);
```

## unsubscribe()

Leave a channel.

```typescript
insforge.realtime.unsubscribe('chat:room-1');
```

`unsubscribe()` is fire-and-forget. If this was the final socket for a logical member, other subscribers receive `presence:leave`.

## disconnect()

Close the WebSocket and clear local subscriptions.

```typescript
insforge.realtime.disconnect();
```

## Message Shape

Delivered messages include your payload fields plus server metadata.

```typescript
import type { SocketMessage } from '@insforge/sdk';

interface OrderStatusMessage extends SocketMessage {
  id: string;
  status: string;
}

insforge.realtime.on<OrderStatusMessage>('status_changed', (message) => {
  console.log(message.id);
  console.log(message.status);
  console.log(message.meta.messageId);
  console.log(message.meta.senderType);
  console.log(message.meta.senderId);
  console.log(message.meta.timestamp);
});
```

Metadata:

```typescript
interface SocketMessageMeta {
  channel?: string;
  messageId: string;
  senderType: 'system' | 'user';
  senderId?: string;
  timestamp: string;
}
```

`senderType` is `system` for database-triggered messages and `user` for client-published messages.

## Presence

A successful subscription returns the current presence snapshot.

```typescript
const response = await insforge.realtime.subscribe('chat:room-1');

if (response.ok) {
  for (const member of response.presence.members) {
    console.log(member.type, member.presenceId, member.joinedAt);
  }
}
```

Presence member:

```typescript
type PresenceMember =
  | {
      type: 'user';
      presenceId: string;
      joinedAt: string;
    }
  | {
      type: 'anonymous';
      presenceId: string;
      joinedAt: string;
    };
```

Listen for changes:

```typescript
insforge.realtime.on('presence:join', (message) => {
  console.log('Joined:', message.member.presenceId);
});

insforge.realtime.on('presence:leave', (message) => {
  console.log('Left:', message.member.presenceId);
});
```

## Properties

| Property | Type | Description |
|----------|------|-------------|
| `isConnected` | `boolean` | Whether the socket is currently connected. |
| `connectionState` | `'disconnected' \| 'connecting' \| 'connected'` | Current SDK connection state. |
| `socketId` | `string \| undefined` | Socket.IO ID when connected. |
| `getSubscribedChannels()` | `() => string[]` | Local list of subscribed channels. |

## Error Handling

```typescript
insforge.realtime.on('connect_error', (error) => {
  console.error('Connection failed:', error.message);
});

insforge.realtime.on('disconnect', (reason) => {
  console.log('Disconnected:', reason);
});

insforge.realtime.on('error', (error) => {
  console.error(error.channel, error.code, error.message);
});
```

Common realtime error codes:

| Code | Meaning |
|------|---------|
| `REALTIME_UNAUTHORIZED` | RLS or channel matching denied subscribe or publish. |
| `REALTIME_NOT_SUBSCRIBED` | The client tried to publish before subscribing to the channel. |
| `INTERNAL_ERROR` | The backend could not complete the realtime operation. |

## Complete Example

```typescript
const channel = `order:${orderId}`;

insforge.realtime.on('error', (error) => {
  console.error(error.code, error.message);
});

await insforge.realtime.connect();

const response = await insforge.realtime.subscribe(channel);

if (!response.ok) {
  throw new Error(response.error.message);
}

insforge.realtime.on('status_changed', (message) => {
  renderOrderStatus(message.status);
});

await insforge.realtime.publish(channel, 'customer_viewed', {
  orderId,
  viewedAt: new Date().toISOString()
});

function cleanup() {
  insforge.realtime.unsubscribe(channel);
  insforge.realtime.disconnect();
}
```
