Skip to main content
View as Markdown

Real-Time Subscriptions

Data-bound page components can stay current without a full reload by subscribing to record changes. A data source declares a refreshModestatic (the default), poll, or realtime — and Sovrium does the rest: poll mode re-fetches on an interval, realtime mode opens a WebSocket (with an SSE fallback) and applies insert/update/delete events as they arrive. External integrations can subscribe programmatically through the same endpoints.

Endpoint Transport
GET /api/tables/:tableSlug/subscribe WebSocket upgrade (per-table)
GET /api/tables/:tableSlug/subscribe/sse Server-Sent Events fallback (per-table)
GET /api/realtime/subscribe Global WebSocket

refreshMode on a data source

Set refreshMode on any component's dataSource. The component's existing filter and sort scope the live stream too — clients only receive changes that still match the data source's criteria.

pages:
  - name: Live Dashboard
    path: /dashboard
    components:
      - type: dataTable
        dataSource:
          table: orders
          filter:
            - field: status
              operator: eq
              value: processing
          sort:
            - field: createdAt
              direction: desc
          refreshMode: realtime

Poll mode

Poll mode periodically re-fetches the data source — no WebSocket needed. Use it when near-real-time is sufficient and connection management overhead is undesirable.

dataSource:
  table: inventory
  refreshMode: poll
  pollIntervalMs: 10000 # Re-fetch every 10 seconds
Property Default Constraint
pollIntervalMs 30000 (30 s) Minimum 1000 (1 s) to prevent server flooding

Real-time mode (WebSocket)

refreshMode: realtime opens a WebSocket to /api/tables/:tableSlug/subscribe. The server pushes change events as records are created, updated, or deleted — from any source (REST API, admin UI, direct DB writes, automations).

GET /api/tables/orders/subscribe
Upgrade: websocket

The server then streams messages such as:

{ "type": "change", "event": "insert", "record": { "id": "42", "fields": {  } } }
{ "type": "change", "event": "update", "record": {  }, "oldRecord": {  } }
{ "type": "change", "event": "delete", "recordId": 42 }
{ "type": "heartbeat", "timestamp": "2026-04-05T12:00:00Z" }

Insert/update events carry the full record (field-permission filtered server-side); delete events carry only recordId. The connection requires authentication (401 on unauthenticated upgrade) and a valid table slug (404 otherwise).

Subscription filtering

Subscriptions respect the data source's current filter and sort, so clients are not flooded with irrelevant events. A change that no longer matches the filter is delivered as the appropriate transition (e.g. an update that moves a row out of the filtered set is surfaced so the client can drop it).

Message contract

All realtime messages — WebSocket frames and SSE data: events use the identical format — are a discriminated union keyed on type.

type Purpose
change insert/update/delete mutation; record/oldRecord use the { id, fields } payload
conflict A concurrent edit overwrote a pending optimistic change (drives the conflict toast)
heartbeat Keep-alive emitted every heartbeatIntervalMs
subscribed Handshake confirmation; echoes the resolved filter/fields/subscriptionId
unsubscribed Unsubscription confirmation
join / leave A user opened/left a presence: true page (scoped by pagePath)
presence-sync Full presence snapshot sent once on join
connection-status Transport connectivity (connected / reconnecting)

The change record payload mirrors the Records API { id, fields } envelope, so a client can apply an event to a data-bound cache with no shape translation. The fields are already field-permission filtered.

Presence awareness

When a page sets presence: true, the server broadcasts join/leave/presence-sync messages so teams can see who else is viewing the same page or table — reducing accidental conflicts. Presence is scoped by pagePath.

Conflict resolution & optimistic locking

Realtime drives an optimistic-UI flow: the client applies its edit immediately, then reconciles with the server. If a concurrent edit overwrote a pending optimistic change, the server emits a conflict message and the canonical (server-wins) value prevails.

A record PATCH may carry a top-level updatedAt token. When present, the server rejects a stale write with 409 Conflict. Tables that participate declare an updated-at system field so the column auto-bumps on every write — see CRUD & Upsert.

Connection management

The client auto-reconnects with the configured backoff, sends/expects heartbeats, and falls back to SSE where WebSocket is unavailable — so realtime features work reliably across network conditions.

Programmatic subscription

External clients open /api/tables/:tableSlug/subscribe (or the global /api/realtime/subscribe) and send a handshake naming the table (and optional subscriptionId, filter, fields). The server replies with a subscribed confirmation, then streams change events; an unsubscribe yields an unsubscribed confirmation.