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
| Guarantee | Value |
|---|---|
| Propagation latency | < 5 seconds (p99 under normal load) |
| Delivery model | At-least-once — duplicate events possible during Debezium failover |
| Ordering | Per-entity ordering guaranteed (Kafka partition key = entityId) |
| Cross-entity ordering | Not guaranteed — events for different records may arrive out of order |
| Data staleness disclosure | lastUpdatedAt 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).