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.
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