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.

app.yaml
pages:
  - name: Live Dashboard
    path: /dashboard
    components:
      - type: data-table
        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.

app.yaml
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).

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

The server then streams messages such as:

app.json
{ "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.

Last updated August 11, 2026

This documentation was written with AI, so errors or outdated content are possible. Sovrium is in beta. Contributions and corrections are welcome.

Built with Sovrium