# InsForge Realtime - Agent Documentation

## Use Realtime For

- Live client updates from database changes.
- Client-to-client broadcasts on named channels.
- Ephemeral presence on subscribed channels.
- Webhook fan-out for messages published to a channel.

Realtime delivers events to subscribed clients and configured webhook URLs. If server-side code must perform work after a database change, such as sending email, writing another table, or calling an API, put that work in an Edge Function and invoke it from a database trigger.

## Mental Model

1. Create channel patterns in `realtime.channels`.
2. Publish messages into `realtime.messages`.
3. Postgres triggers `pg_notify('realtime_message', message_id)`.
4. The backend loads the message, checks the channel is enabled, and delivers it to Socket.IO subscribers plus channel webhooks.
5. Delivery stats are written back to the message row.

Channel patterns use SQL `LIKE`: `order:%` matches `order:123`. Use `:` as the separator and `%` as the wildcard. Do not use `_`.

## Backend Setup

### 1. Create Channel Patterns

```sql
INSERT INTO realtime.channels (pattern, description, enabled)
VALUES
  ('orders', 'Global order events', true),
  ('order:%', 'Per-order events', true),
  ('chat:%', 'Chat room events', true);
```

You can also create channels in the Dashboard Realtime page.

### 2. Publish Database Changes

Create a trigger on the app table you want to watch. In the trigger function, call `realtime.publish(channel, event, payload)` to choose the channel, event name, and payload.

```sql
CREATE OR REPLACE FUNCTION notify_order_status()
RETURNS TRIGGER AS $$
BEGIN
  PERFORM realtime.publish(
    'order:' || NEW.id::text,
    'status_changed',
    jsonb_build_object(
      'id', NEW.id,
      'status', NEW.status,
      'updatedAt', NEW.updated_at
    )
  );
  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER order_status_realtime
  AFTER UPDATE OF status ON orders
  FOR EACH ROW
  WHEN (OLD.status IS DISTINCT FROM NEW.status)
  EXECUTE FUNCTION notify_order_status();
```

For delete events, use `OLD` instead of `NEW`.

### 3. Add Access Control When Needed

Realtime is open by default. `anon` and `authenticated` can subscribe to enabled channels and publish to channels they joined.

To restrict access:

```sql
ALTER TABLE realtime.channels ENABLE ROW LEVEL SECURITY;
ALTER TABLE realtime.messages ENABLE ROW LEVEL SECURITY;
```

Subscribe is controlled by `SELECT` policies on `realtime.channels`:

```sql
CREATE POLICY "users_subscribe_own_orders"
ON realtime.channels
FOR SELECT
TO authenticated
USING (
  pattern = 'order:%'
  AND EXISTS (
    SELECT 1
    FROM orders
    WHERE id = NULLIF(split_part(realtime.channel_name(), ':', 2), '')::uuid
      AND user_id = auth.uid()
  )
);
```

Publish is controlled by `INSERT` policies on `realtime.messages`:

```sql
CREATE POLICY "members_publish_chat"
ON realtime.messages
FOR INSERT
TO authenticated
WITH CHECK (
  channel_name LIKE 'chat:%'
  AND EXISTS (
    SELECT 1
    FROM chat_members
    WHERE room_id = NULLIF(split_part(channel_name, ':', 2), '')::uuid
      AND user_id = auth.uid()
  )
);
```

Use `realtime.channel_name()` in subscribe policies because `realtime.channels` stores patterns, while the client requests a resolved channel such as `order:123`.

## Frontend SDK Pattern

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

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

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

await insforge.realtime.connect();

const response = await insforge.realtime.subscribe(`order:${orderId}`);

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

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

### SDK Methods

| Task | Method |
|------|--------|
| Connect | `await insforge.realtime.connect()` |
| Subscribe | `await insforge.realtime.subscribe(channel)` |
| Publish | `await insforge.realtime.publish(channel, event, payload)` |
| Listen | `insforge.realtime.on(event, callback)` |
| Listen once | `insforge.realtime.once(event, callback)` |
| Remove listener | `insforge.realtime.off(event, callback)` |
| Unsubscribe | `insforge.realtime.unsubscribe(channel)` |
| Disconnect | `insforge.realtime.disconnect()` |
| List local subscriptions | `insforge.realtime.getSubscribedChannels()` |

Client publish requires a successful subscription to the same channel first.

## Raw Socket.IO Contract

Use this only when the SDK is not available.

```typescript
import { io } from 'socket.io-client';

const socket = io('https://your-project.insforge.app', {
  auth: {
    token: '<user-jwt-or-anon-token>'
  }
});

socket.emit('realtime:subscribe', { channel: 'chat:room-1' }, (response) => {
  if (!response.ok) {
    console.error(response.error);
  }
});

socket.on('new_message', (message) => {
  console.log(message);
});

socket.emit('realtime:publish', {
  channel: 'chat:room-1',
  event: 'new_message',
  payload: { text: 'Hello' }
});
```

Events:

| Event | Direction |
|-------|-----------|
| `realtime:subscribe` | Client to server |
| `realtime:unsubscribe` | Client to server |
| `realtime:publish` | Client to server |
| Custom event name | Server to client |
| `presence:join` | Server to client |
| `presence:leave` | Server to client |
| `realtime:error` | Server to client |

## Presence

`subscribe()` returns:

```json
{
  "ok": true,
  "channel": "chat:room-1",
  "presence": {
    "members": [
      {
        "type": "user",
        "presenceId": "user-id",
        "joinedAt": "2026-04-25T17:00:00.000Z"
      }
    ]
  }
}
```

Listen for `presence:join` and `presence:leave` to keep local online state current. Presence is in-memory online state, not durable membership.

## Webhooks

Channel `webhookUrls` receive the message payload as the request body.

Headers:

| Header | Meaning |
|--------|---------|
| `X-InsForge-Event` | Event name |
| `X-InsForge-Channel` | Resolved channel name |
| `X-InsForge-Message-Id` | Message UUID |

Webhook delivery counts appear in message history as `whAudienceCount` and `whDeliveredCount`.

## Message Retention

`realtime.config.retention_days` controls cleanup:

- `NULL`: keep messages indefinitely.
- Positive integer: delete messages older than that many days.

The cleanup job runs daily through pg_cron.

## Dashboard And REST Checks

Use Dashboard Realtime pages to verify:

- Channels: patterns, enabled state, webhooks.
- Messages: payloads, sender type, WebSocket audience, webhook delivery.
- Permissions: RLS policies for subscribe and publish.
- Settings: message retention.

REST endpoints:

| Endpoint | Purpose |
|----------|---------|
| `GET /api/realtime/channels` | List channels |
| `POST /api/realtime/channels` | Create channel |
| `PUT /api/realtime/channels/{id}` | Update channel |
| `DELETE /api/realtime/channels/{id}` | Delete channel |
| `GET /api/realtime/messages` | List message history |
| `DELETE /api/realtime/messages` | Clear all message history |
| `GET /api/realtime/messages/stats` | Message stats |
| `GET /api/realtime/permissions` | Realtime RLS policies |
| `GET /api/realtime/config` | Retention config |
| `PATCH /api/realtime/config` | Update retention config |
