Real-Time Events (REN)
REN (Resource Event Notifications) is Maravilla’s built-in real-time event system. It delivers platform resource mutations — KV writes, database changes, storage uploads — to connected clients via Server-Sent Events (SSE).
Use REN to build live dashboards, collaborative features, or any UI that needs to react to data changes without polling.
Subscribe, don’t poll
This is the default way to stay current on Maravilla, not an optimisation to add later. Your client already holds one connection, and the platform pushes an event the moment anything changes — so being told is cheaper and faster than asking.
A setInterval that re-reads a value is a subscription written the expensive way: one request per tick per open tab, and still up to one interval stale. Anything long-running here announces its own progress — media transforms emit transform.*, storage emits storage.*, the database emits db.document.* — precisely so you can watch instead of interrogate.
Polling is defensible in two places, both server-side:
- a third-party API with no push — poll it from a scheduled handler, one step per invocation, and republish each step to a channel so every client is pushed rather than polling too;
- a bounded read-back to confirm a write landed.
Everything else — and every customer-facing UI — subscribes. Which of the two transports you want:
| REN (this page) | Realtime Channels | |
|---|---|---|
| Carries | platform data change you did not author: kv.*, db.document.*, storage.*, transform.* | your own messages: chat, cursors, “the run advanced”, presence |
| Transport | SSE | WebSocket |
| Published by | the platform | your server code, via platform.realtime.publish() |
How It Works
- Your client opens an SSE connection to
/api/maravilla/ren - When any platform service mutates data (KV put, DB insert, storage upload), an event is published
- Connected clients receive the event in real time
- Clients can filter by resource type and distinguish their own mutations from others
In multi-node deployments, events are distributed across all nodes automatically.
Quick Start
Using the REN Client
Import RenClient from the platform package:
import { RenClient } from '@maravilla-labs/platform/ren';
const ren = new RenClient({
subscriptions: ['kv', 'db'], // only receive KV and database events
});
const unsubscribe = ren.on((event) => {
console.log(event.t, event.k); // e.g. "kv.put", "todo:abc123"
});
// Later: clean up
unsubscribe();
ren.close();
Using Native EventSource
You can also use the browser’s EventSource API directly:
const es = new EventSource('/api/maravilla/ren?s=kv,storage');
es.addEventListener('kv.put', (e) => {
const event = JSON.parse(e.data);
console.log('KV updated:', event.k);
});
es.addEventListener('storage.object.created', (e) => {
const event = JSON.parse(e.data);
console.log('File uploaded:', event.k);
});
Event Schema
Every REN event has the following structure:
interface RenEvent {
t: string; // event type, e.g. "kv.put", "db.document.created"
r: string; // resource domain: "kv", "db", "storage", "realtime", "presence"
k?: string; // resource key (e.g. object key, document ID)
ns?: string; // namespace, collection, or bucket name
v?: string; // version or etag
ts?: number; // timestamp in unix milliseconds
src?: string; // origin client/node ID (for self-filtering)
ch?: string; // channel name (for realtime channels)
data?: any; // arbitrary payload (for realtime messages)
uid?: string; // user identity (for presence events)
}
Event Types
KV Store Events
| Event Type | Fired When |
|---|---|
kv.put | A key-value pair is created or updated |
kv.delete | A key is deleted |
kv.expired | A key expires via TTL |
Database Events
| Event Type | Fired When |
|---|---|
db.document.created | A document is inserted |
db.document.updated | A document is updated |
db.document.deleted | A document is deleted |
Storage Events
| Event Type | Fired When |
|---|---|
storage.put | A file is uploaded or overwritten (or a presigned upload is confirmed) |
storage.delete | A file is deleted |
Media Transform Events
Fired during media transform jobs (transcode, thumbnail, resize, OCR). Subscribe to update loading states, progress bars, or swap placeholder posters the moment the output lands.
| Event Type | Fired When |
|---|---|
transform.queued | A transform job has been accepted; outputKey is already known |
transform.started | A worker picked up the job |
transform.progress | Periodic update during long-running work ({ percent, stage }) |
transform.complete | Output is written to storage |
transform.failed | Terminal failure after retries are exhausted |
Realtime Channel Events
| Event Type | Fired When |
|---|---|
realtime.message | A message is published to a channel |
Presence Events
| Event Type | Fired When |
|---|---|
presence.join | A user joins a channel |
presence.update | A user’s metadata changes |
presence.leave | A user leaves a channel |
For channels and presence, see the full Realtime Channels documentation.
RenClient Options
const ren = new RenClient({
endpoint: '/api/maravilla/ren', // SSE endpoint (auto-detected)
subscriptions: ['kv', 'db'], // resource filters, ['*'] = all (default)
clientId: 'my-client-id', // optional, auto-generated if omitted
autoReconnect: true, // reconnect on disconnect (default: true)
maxBackoffMs: 15000, // max reconnect delay (default: 15s)
debug: false, // enable console debug logging
});
Subscription Filtering
Filter which resource domains you receive events for:
// All events (default)
new RenClient({ subscriptions: ['*'] });
// Only KV and storage events
new RenClient({ subscriptions: ['kv', 'storage'] });
// Only database events
new RenClient({ subscriptions: ['db'] });
You can also filter via the SSE URL query parameter:
/api/maravilla/ren?s=kv,storage
/api/maravilla/ren?s=*
Self-Filtering
Use the src field to distinguish your own mutations from others. Pass a client ID header on mutations using renFetch:
import { renFetch, RenClient } from '@maravilla-labs/platform/ren';
const ren = new RenClient();
// Use renFetch for mutations — it adds X-Ren-Client header
await renFetch('/api/todos', {
method: 'POST',
body: JSON.stringify({ text: 'Buy groceries' }),
});
// In your event handler, filter out self-originated events
ren.on((event) => {
if (event.src === ren.getClientId()) return; // skip own mutations
// Handle event from another client/tab
refreshUI();
});
Auto-Reconnect
The REN client automatically reconnects with exponential backoff when the connection drops:
- First retry: 1 second
- Subsequent retries: doubles each time
- Maximum delay: 15 seconds (configurable via
maxBackoffMs) - Backoff resets on successful connection
The client ID persists in localStorage across reconnects and page reloads.
SSE Endpoint Reference
GET /api/maravilla/ren
Opens a Server-Sent Events stream.
Query Parameters:
s— comma-separated resource filters (e.g.,kv,storage). Use*or omit for all events.cid— client ID for correlation
Response: text/event-stream with events formatted as:
event: kv.put
data: {"t":"kv.put","r":"kv","k":"todo:abc","ns":"demo","ts":1710000000000}
Heartbeat pings (:ping comments) are sent every 15 seconds to keep the connection alive.
Example: Live Todo List
Build a todo list that updates in real time across browser tabs:
// In your Svelte component or page script
import { RenClient } from '@maravilla-labs/platform/ren';
import { invalidateAll } from '$app/navigation';
const ren = new RenClient({
subscriptions: ['kv'],
});
ren.on((event) => {
if (event.src === ren.getClientId()) return;
// Another tab/user modified a todo — refresh the list
if (event.k?.startsWith('todo:')) {
invalidateAll(); // SvelteKit: re-run all load functions
}
});
Development vs. Production
| Development | Production | |
|---|---|---|
| Endpoint | http://localhost:3001/api/maravilla/ren | /api/maravilla/ren |
| Fan-out | Single-process | Distributed across all nodes |
| Detection | Automatic (Vite port 5173 → dev server port 3001) | Automatic (relative URL) |
The RenClient auto-detects the correct endpoint based on the runtime environment.
Debugging
Enable debug logging to see connection state, events, and reconnect attempts:
new RenClient({ debug: true });
Or set localStorage.setItem('REN_DEBUG', '1') in the browser console.
Next Steps
- Realtime Channels — pub/sub channels, presence tracking, and WebSocket API for building chat, collaboration, and live features
- KV Store — key-value storage that triggers REN events on writes
- Database — document database with REN change notifications