> ## Documentation Index
> Fetch the complete documentation index at: https://docs.insforge.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# TypeScript realtime SDK reference

> Subscribe to channels, publish events, track presence, and handle reconnects and token refreshes over Socket.IO with the InsForge TypeScript SDK.

## Installation

<CodeGroup>
  ```bash npm theme={null}
  npm install @insforge/sdk@latest
  ```

  ```bash yarn theme={null}
  yarn add @insforge/sdk@latest
  ```

  ```bash pnpm theme={null}
  pnpm add @insforge/sdk@latest
  ```
</CodeGroup>

```javascript theme={null}
import { createClient } from '@insforge/sdk';

const insforge = createClient({
  baseUrl: 'https://your-app.insforge.app',
  anonKey: 'your-anon-key'  // Optional: for public/unauthenticated requests
});
```

Find the anon key with `npx @insforge/cli secrets get ANON_KEY`, or in the dashboard: click **Install** and open **API Keys**.

## 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 theme={null}
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 theme={null}
await insforge.realtime.connect();
```

Returns:

```typescript theme={null}
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 theme={null}
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 theme={null}
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.

`subscribe()` is idempotent: calling it again for a channel you already joined re-requests the subscription and resolves the server's current presence snapshot. The server tracks presence per logical member, so repeated subscribes never produce duplicate members or spurious `presence:join` events.

## publish()

Publish an event to a channel.

```typescript theme={null}
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 theme={null}
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.                                                                               |
| `presence:sync`  | `PresenceSyncEvent`    | A fresh presence snapshot for a channel. Fired on every successful subscribe, including automatic resubscribes after a reconnect. |

## once()

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

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

## off()

Remove an event listener.

```typescript theme={null}
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 theme={null}
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 theme={null}
insforge.realtime.disconnect();
```

## Message shape

Delivered messages include your payload fields plus server metadata.

```typescript theme={null}
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 theme={null}
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 theme={null}
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 theme={null}
type PresenceMember =
  | {
      type: 'user';
      presenceId: string;
      joinedAt: string;
    }
  | {
      type: 'anonymous';
      presenceId: string;
      joinedAt: string;
    };
```

Listen for changes:

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

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

### Reading presence state

The SDK maintains the member list for every subscribed channel — seeded from the subscribe snapshot and kept current from `presence:join`/`presence:leave` deltas and reconnect resyncs. Read it at any time instead of merging deltas yourself:

```typescript theme={null}
const members = insforge.realtime.getPresenceState('chat:room-1');
```

Returns an empty array for channels you are not subscribed to. While the socket is briefly disconnected it holds the last known state, replaced by a fresh snapshot as soon as the channel resubscribes.

### Reconnects

When the connection drops, the SDK automatically resubscribes to every channel on reconnect and emits `presence:sync` with the fresh presence snapshot for each. Members who joined or left while you were disconnected produce no individual `presence:join`/`presence:leave` deltas, so replace — don't merge — any state you keep outside the SDK:

```typescript theme={null}
import type { PresenceSyncEvent } from '@insforge/sdk';

insforge.realtime.on<PresenceSyncEvent>('presence:sync', ({ channel, presence }) => {
  replaceMembers(channel, presence.members);
});
```

If you render directly from `getPresenceState()`, `presence:sync` is simply your re-render signal. If a resubscribe is rejected (for example the user's access was revoked while disconnected), the SDK drops the channel and emits `error` instead.

### Token refreshes

When the signed-in user's access token refreshes, the SDK leaves an established socket connected — there is no in-band re-authentication, reconnect, or presence churn. A later initial connection or network reconnect obtains the latest access token before its handshake. Sign-in, sign-out, or switching users reconnects the socket under the new identity.

Access *reductions* apply lazily on a live connection. Publishing is re-checked against RLS on every message, and new subscribes are re-checked at join time. Continued receipt on already-joined channels persists until the client unsubscribes or reconnects.

## 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.                              |
| `getPresenceState(channel)` | `(channel: string) => PresenceMember[]`         | Current members of a subscribed channel, maintained by the SDK. |

## Error handling

```typescript theme={null}
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 theme={null}
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();
}
```
