Files
wehub-resource-sync 3a28426bf4
Lint and Format Check / lint-and-format (push) Failing after 0s
Check Migrations / Check for duplicate migration numbers (push) Failing after 1s
CI Pre-merge Check / CI Pre-merge Check (push) Failing after 2m17s
chore: import upstream snapshot with attribution
2026-07-13 12:23:40 +08:00

7.3 KiB

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

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.

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:

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:

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:

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

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.

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:

{
  "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