Skip to main content

Overview

InsForge Realtime provides a powerful event-driven messaging system that bridges database changes to connected clients. Events are triggered by PostgreSQL triggers, delivered through WebSocket connections via Socket.IO, and optionally forwarded to webhook endpoints.

Technology Stack

Core Components

ComponentTechnologyPurpose
Realtime SchemaPostgreSQLChannels, messages tables and publish function
Permission ModelGrants + RLS (optional)Controls subscribe and publish access
Notification Bridgepg_notifyBridges database to Node.js process
Realtime ManagerNode.jsListens for notifications, dispatches messages
WebSocket ServerSocket.IOBidirectional client communication
Webhook SenderAxiosHTTP delivery to external endpoints
SDK@insforge/sdkClient-side subscription and messaging

Database Schema

Channels Table

The realtime.channels table defines available channel patterns and their configuration:
ColumnTypeDescription
idUUIDPrimary key
patternTEXTChannel name pattern (e.g., orders, order:%)
descriptionTEXTHuman-readable description
webhook_urlsTEXT[]Array of webhook endpoints
enabledBOOLEANWhether the channel is active
Channel patterns use : as separator and % for wildcards (SQL LIKE pattern). For example, order:% matches order:123, order:456, etc.

Messages Table

The realtime.messages table stores all published messages for audit purposes:
ColumnTypeDescription
idUUIDPrimary key
event_nameTEXTEvent type (e.g., order_created)
channel_idUUIDReference to channels table
channel_nameTEXTResolved channel name (e.g., order:123)
payloadJSONBEvent data
sender_typeTEXTsystem (trigger) or user (client)
sender_idUUIDUser ID for client-initiated messages
ws_audience_countINTEGERWebSocket subscribers at time of delivery
wh_audience_countINTEGERWebhook URLs configured
wh_delivered_countINTEGERSuccessful webhook deliveries

Permission Model

InsForge Realtime uses PostgreSQL grants and optional Row Level Security (RLS) to control channel access.
RLS is disabled by default for the best developer experience. Channels and messages are open to all authenticated and anonymous users out of the box. Enable RLS when you need fine-grained access control.

Default Permissions (RLS Disabled)

PermissionTableGrantAccess
Subscriberealtime.channelsSELECTAll authenticated/anon users
Publishrealtime.messagesINSERTAll authenticated/anon users

Enabling Access Control

To restrict access, enable RLS and add policies:
-- Step 1: Enable RLS
ALTER TABLE realtime.channels ENABLE ROW LEVEL SECURITY;
ALTER TABLE realtime.messages ENABLE ROW LEVEL SECURITY;

-- Step 2: Add policies (see examples below)

How RLS Works (When Enabled)

PermissionTablePolicy TypeDescription
Subscriberealtime.channelsSELECTWho can subscribe to a channel
Publishrealtime.messagesINSERTWho can publish to a channel
  1. When a client subscribes, the backend executes a SELECT query on realtime.channels with the user’s role
  2. RLS policies filter the result. If a matching channel is returned, subscription is allowed
  3. When a client publishes, the backend attempts an INSERT into realtime.messages
  4. RLS policies evaluate the INSERT. If allowed, the message is stored and broadcast

Helper Function

Use realtime.channel_name() in your policies to access the channel being requested:
-- Allow authenticated users to subscribe to their own order channels
CREATE POLICY "users_can_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()
  )
);

Message Flow

System Events (Database Triggers)

Client Events (User-Initiated)

Publish Function

The realtime.publish() function is called by your database triggers:
CREATE OR REPLACE FUNCTION notify_order_changes()
RETURNS TRIGGER AS $$
BEGIN
  PERFORM realtime.publish(
    'order:' || NEW.id::text,           -- channel name
    TG_OP || '_order',                   -- event: INSERT_order, UPDATE_order, etc.
    jsonb_build_object(
      'id', NEW.id,
      'status', NEW.status,
      'total', NEW.total
    )
  );
  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER order_changes_trigger
  AFTER INSERT OR UPDATE ON orders
  FOR EACH ROW
  EXECUTE FUNCTION notify_order_changes();
The realtime.publish() function can only be called by database triggers (SECURITY DEFINER). It bypasses RLS to insert system messages.

WebSocket Events

Client-to-Server Events

EventPayloadDescription
realtime:subscribe{ channel: string }Subscribe to a channel
realtime:unsubscribe{ channel: string }Unsubscribe from a channel
realtime:publish{ channel, event, payload }Publish a message

Server-to-Client Events

EventPayloadDescription
{eventName}Message payload + metaCustom event from channel
realtime:error{ code, message }Error notification

Socket Message Structure

All WebSocket messages include a meta object with server-enforced fields alongside the event payload:
interface SocketMessage {
  meta: {
    channel?: string           // Channel the message was sent to
    messageId: string          // Unique message ID (UUID)
    senderType: 'system' | 'user'
    senderId?: string          // User ID for client messages
    timestamp: Date            // Server timestamp
  }
  // ...event payload fields
}

Webhook Delivery

When channels have webhook_urls configured, messages are delivered via HTTP POST:

Request Format

POST /your-webhook-endpoint HTTP/1.1
Content-Type: application/json
X-InsForge-Event: order_created
X-InsForge-Channel: order:123
X-InsForge-Message-Id: a1b2c3d4-e5f6-...

{
  "id": "123",
  "status": "confirmed",
  "total": 99.99
}

Delivery Guarantees

FeatureBehavior
Retries2 retries with 1s, 2s backoff
Timeout10 seconds per request
ParallelAll webhook URLs called concurrently
TrackingSuccess/failure counts stored per message

Sender Types

TypeSourceDescription
systemDatabase triggersEvents from realtime.publish() function
userClient SDKEvents from insforge.realtime.publish()
System events are trusted and bypass publish RLS checks. User events must pass INSERT policy on realtime.messages.

Developer Workflow

1

Define Channels

Create channel patterns in realtime.channels:
INSERT INTO realtime.channels (pattern, description)
VALUES ('order:%', 'Order-specific events');
2

Configure Permissions (Optional)

RLS is disabled by default, so all users can subscribe and publish. To restrict access, enable RLS and add policies:
-- Step 1: Enable RLS
ALTER TABLE realtime.channels ENABLE ROW LEVEL SECURITY;
ALTER TABLE realtime.messages ENABLE ROW LEVEL SECURITY;

-- Step 2: Add policies
-- Subscribe: users can only subscribe to their own order 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: only admins can publish to order channels
CREATE POLICY "admins_publish_orders"
ON realtime.messages FOR INSERT
TO authenticated
WITH CHECK (
  channel_name LIKE 'order:%'
  AND EXISTS (
    SELECT 1 FROM admins
    WHERE user_id = auth.uid()
  )
);
3

Create Triggers

Add trigger function and trigger to emit events on database changes:
CREATE OR REPLACE FUNCTION notify_order_changes()
RETURNS TRIGGER AS $$
BEGIN
  PERFORM realtime.publish(
    'order:' || NEW.id::text,
    TG_OP || '_order',
    jsonb_build_object('id', NEW.id, 'status', NEW.status)
  );
  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER order_realtime
AFTER INSERT OR UPDATE ON orders
FOR EACH ROW EXECUTE FUNCTION notify_order_changes();
4

Subscribe in Client

Use the SDK to subscribe and listen:
await insforge.realtime.connect()
await insforge.realtime.subscribe('order:123')
insforge.realtime.on('UPDATE_order', (data) => {
  console.log('Order updated:', data)
})

Architecture Features

Database-Driven

Events originate from PostgreSQL triggers, ensuring consistency with your data

Optional RLS Security

Works out of the box, with optional RLS for fine-grained access control

Dual Delivery

Messages delivered to both WebSocket clients and webhook endpoints simultaneously

Audit Trail

All messages stored in database with delivery statistics for debugging and replay

Pattern Matching

Wildcard channel patterns let you define permissions for dynamic channels

Bidirectional

Clients can both receive events and publish their own messages (subject to RLS)

Performance Characteristics

MetricValueNotes
Latency~10-50msDatabase to client, depends on network
ThroughputHighLimited by PostgreSQL NOTIFY and Socket.IO
PersistenceFullAll messages stored in database
ReconnectionAutomaticSocket.IO handles reconnection
Webhook Timeout10sPer webhook request

Best Practices

Keep Payloads Small

Only include necessary data in payloads

Use Specific Channels

Prefer order:123 over broadcasting to orders for reducing the traffic

Add RLS When Needed

Enable RLS and add policies when you need to restrict channel access

Monitor Delivery

Check ws_audience_count and wh_delivered_count to debug delivery issues

Handle Reconnection

Design clients to refetch state on reconnect since missed messages are not replayed

Use Webhooks for Reliability

For critical notifications, configure webhook URLs as backup delivery method

Limitations

  • No Message Replay: Clients don’t receive messages missed during disconnection
  • No Presence: No built-in tracking of who’s online in a channel
  • Single Region: Messages delivered from single backend instance
  • Webhook Retries: Limited to 2 retries with short timeout