Real-Time Subscriptions
Data-bound page components can stay current without a full reload by subscribing to record changes. A data source declares a refreshMode — static (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.
Frozen transport constants. Client and server share REALTIME_TRANSPORT_CONFIG: reconnect backoff [1000, 2000, 4000, 8000] ms, max reconnect delay 30000 ms, heartbeat 30000 ms, max 10 connections per user, idle timeout 300000 ms, presence-stale timeout 60000 ms, max 50 presence entries per page.
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.
Related pages
- CRUD & Upsert — the optimistic-locking
updatedAtcontract - Filtering, Sorting & Pagination — the filter/sort that scope a subscription
- Records Overview — the
{ id, fields }envelope the stream mirrors - Pages Overview — data-bound components and
dataSourceconfiguration