Moul

Real-Time SSE Subscriptions

High-performance Server-Sent Events (SSE) subscriptions for live data synchronization without polling.

Moul provides built-in Server-Sent Events (SSE) streaming for real-time reactivity. Whenever dynamic records are created, updated, or deleted, subscribed clients receive instant notification events over persistent HTTP streams.


Subscription Endpoints

1. Single Collection Subscription

GET /api/moul/:name/subscribe

Query Parameters:

  • event: Comma-separated list of event actions to filter (create, update, delete, or *). Default: *.
  • id: Optional filter for a specific record ID.
  • ticket: Optional short-lived single-use SSE ticket obtained via POST /api/realtime/ticket.

Security Note: Passing credentials in URL query strings (?token= or ?admin_key=) is strictly disabled and returns HTTP 400 Bad Request to prevent exposure in proxy logs, browser histories, and Referer headers. Always stream via headers (Authorization: Bearer) or exchange credentials for a 30-second single-use ticket.

2. Multi-Collection Subscription

GET /api/moul/subscribe?mouls=posts,comments,notifications&ticket=tkt_...


Client Integration Examples

Browser Client: Single-Use Ticket Exchange

// 1. Request an opaque, 30-second single-use ticket
const { ticket } = await fetch('/api/realtime/ticket', {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${userToken}` },
}).then(r => r.json());

// 2. Open standard EventSource without URL credential leakage
const sseUrl = `/api/moul/posts/subscribe?event=create,update,delete&ticket=${ticket}`;
const evtSource = new EventSource(sseUrl);

// Listen for new record insertions
evtSource.addEventListener('create', (event) => {
  const data = JSON.parse(event.data);
  console.log('New record created:', data.record);
});

// Listen for updates
evtSource.addEventListener('update', (event) => {
  const data = JSON.parse(event.data);
  console.log('Record updated:', data.record);
});

// Listen for deletions or transition-out events
evtSource.addEventListener('delete', (event) => {
  const data = JSON.parse(event.data);
  console.log('Record removed or visibility revoked with ID:', data.record.id);
});

// Listen for mid-stream token expiration or revocation
evtSource.addEventListener('auth_expired', (event) => {
  const data = JSON.parse(event.data);
  console.warn('Session expired or revoked:', data.message);
  evtSource.close();
  // Redirect to login or initiate JWT refresh
});

evtSource.onerror = (err) => {
  console.error('SSE connection error:', err);
};

React Hook Example

import { useEffect, useState } from 'react';

export function useRecordSubscription<T>(collection: string, initialData: T[]) {
  const [records, setRecords] = useState<T[]>(initialData);

  useEffect(() => {
    const evtSource = new EventSource(`/api/moul/${collection}/subscribe`);

    evtSource.addEventListener('create', (e) => {
      const { record } = JSON.parse(e.data);
      setRecords((prev) => [record, ...prev]);
    });

    evtSource.addEventListener('update', (e) => {
      const { record } = JSON.parse(e.data);
      setRecords((prev) => prev.map((r: any) => (r.id === record.id ? record : r)));
    });

    evtSource.addEventListener('delete', (e) => {
      const { record } = JSON.parse(e.data);
      setRecords((prev) => prev.filter((r: any) => r.id !== record.id));
    });

    return () => {
      evtSource.close();
    };
  }, [collection]);

  return records;
}

cURL CLI Streaming (Header Auth or Ticket)

# Direct stream using standard Authorization Bearer header
curl -N -H "Authorization: Bearer <USER_JWT>" "http://localhost:8090/api/moul/posts/subscribe"

# Direct stream using Admin Key header
curl -N -H "X-Admin-Key: <ADMIN_KEY>" "http://localhost:8090/api/moul/posts/subscribe"

# Stream using short-lived single-use ticket
curl -N "http://localhost:8090/api/moul/posts/subscribe?ticket=tkt_98a7f..."

{
  "action": "create",
  "moul": "posts",
  "record": {
    "id": "rec_01J6XYZ123",
    "title": "Realtime Streams in Moul",
    "slug": "realtime-streams-in-moul",
    "published": true,
    "createdAt": "2026-08-01T12:00:00Z",
    "updatedAt": "2026-08-01T12:00:00Z"
  }
}

Encrypted Cloak Field Masking & Stream Security

When collections contain encrypted cloak fields (e.g. SSNs, API secrets, financial information, or PII), the realtime hub guarantees privacy across persistent broadcast channels:

  1. Automatic Masking (••••1234): All cloak fields are decrypted in-flight and serialized using Moul's masking algorithm (••••1234 or ••••), guaranteeing neither raw ciphertext nor plaintext secrets are broadcast to standard clients.
  2. Companion Hash Stripping: Companion blind-index columns (<fieldName>Hash, e.g. ssnHash) and internal auth credentials (passwordHash, otpCode, passkeys) are stripped unconditionally before frames reach client channels.
  3. Strict Reveal Restrictions: Non-admin clients specifying ?reveal=all or any reveal parameter on subscription endpoints (/api/moul/:name/subscribe, /api/moul/subscribe, or /api/realtime/ticket) receive HTTP 400 Bad Request: "The reveal parameter is not permitted on realtime broadcast streams".
  4. Authorized Admin Reveal: Authenticated administrators (root accounts or X-Admin-Key) may explicitly request unmasked plaintext by supplying ?reveal=all (or a comma-separated field list) either when generating a ticket (POST /api/realtime/ticket?reveal=all) or directly upon connecting to the SSE stream. Admins without ?reveal default to masked values for defense-in-depth.

On this page