Skip to main content

Real-time Subscriptions

The platform delivers real-time data changes to browser clients via a CDC → WebSocket bridge. When a record changes in PostgreSQL, Debezium captures the WAL event, publishes it to Kafka (platform.data.events), and the Notify service fans it out over WebSocket to connected browser sessions that have subscribed to that model.

Modules never manage WebSocket connections directly. The useRealtime() SDK hook handles subscriptions, reconnection, and tenant-scoped filtering automatically.


Pipeline

1. Module code calls data.create() / update() / delete()

│ PostgreSQL WRITE

PostgreSQL OLTP

│ WAL (Write-Ahead Log) captured by Debezium

Debezium

│ Kafka topic: platform.data.events

Notify Service (WebSocket fan-out)

│ WebSocket push (tenant-scoped)

Browser client (useRealtime() hook)

Latency from database write to browser update: < 5 seconds under normal load. This is eventual consistency — the browser may momentarily show stale data while the pipeline propagates.


Subscription Model

Real-time subscriptions are model-level, not record-level. A subscription to contacts receives events for all records in the tenant that the current user has {module}.{model}.read permission for. Tenant isolation is enforced automatically — a subscriber only receives events for records belonging to their own tenant.

The CDC event payload carries the full record state after the change (not a delta) so hooks can update the UI without fetching the record again.


SDK — useRealtime()

import { useRealtime } from '@platform/sdk-data';

// Subscribe to all changes on the 'contacts' model
function ContactList() {
const { records, isConnected, lastUpdatedAt } = useRealtime('contacts', {
onCreated: (record) => {
console.log('New contact:', record.id);
},
onUpdated: (record) => {
console.log('Updated contact:', record.id);
},
onDeleted: (record) => {
console.log('Soft-deleted contact:', record.id);
},
});

if (!isConnected) return <span>Reconnecting...</span>;

return (
<ul>
{records.map((c) => <li key={c.id}>{c.name}</li>)}
</ul>
);
}

The hook maintains the WebSocket connection for the component's lifetime and automatically cleans up on unmount. Reconnection uses exponential backoff (initial 1 s, max 30 s, jitter ±500 ms).

Subscription with filter

Server-side filtering reduces the number of events delivered to the client. Only events matching the filter are pushed over WebSocket:

const { records } = useRealtime('contacts', {
filter: { status: 'eq.active' },
onUpdated: (record) => { /* ... */ },
});

Manual subscription (server-to-server)

For Go-based module services that need to react to model changes in real time:

// services/my-module/internal/handlers/contacts.go
err := kernel.Data().Subscribe(ctx, "contacts", func(event data.ChangeEvent) {
switch event.Operation {
case data.OpCreate:
// handle created record
case data.OpUpdate:
// handle updated record
case data.OpDelete:
// handle soft-deleted record
}
})

WebSocket Connection

The Notify service manages WebSocket connections on behalf of all modules. Browser clients connect once per session:

wss://api.septemcore.com/v1/ws
Authorization: Bearer <access_token>

The server authenticates via JWT on connection. The tenantId from the JWT is used to scope all subsequent real-time events — a client never receives events from a different tenant even if they construct a crafted subscription message.


Consistency Guarantees

GuaranteeValue
Propagation latency< 5 seconds (p99 under normal load)
Delivery modelAt-least-once — duplicate events possible during Debezium failover
OrderingPer-entity ordering guaranteed (Kafka partition key = entityId)
Cross-entity orderingNot guaranteed — events for different records may arrive out of order
Data staleness disclosurelastUpdatedAt field returned by useRealtime() hook

Reconnection and Replay

If the WebSocket connection drops, the SDK reconnects automatically. On reconnect, the hook fetches the latest snapshot of the model via data.list() to fill any gap that occurred during disconnection, then resumes streaming from the live CDC feed. This prevents stale state after network interruptions.


Required Permissions

A subscription to contacts requires {module}.contacts.read. The Notify service validates the JWT on connection and revalidates tenantId on every subscription request. Subscribing to a model the user lacks read permission for silently drops the subscription (no error — avoids model enumeration).