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: data-table
dataSource:
table: orders
filter:
- field: status
operator: eq
value: processing
sort:
- field: createdAt
direction: desc
refreshMode: realtimePoll 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: websocketThe 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
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.