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", "runtime"
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)
}
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.object.created | A file is uploaded |
storage.object.updated | A file is overwritten |
storage.object.deleted | A file is deleted |
Runtime Events
| Event Type | Fired When |
|---|---|
runtime.snapshot.ready | A snapshot is ready |
runtime.worker.started | A worker isolate starts |
runtime.worker.stopped | A worker isolate stops |
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.