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

  1. Your client opens an SSE connection to /api/maravilla/ren
  2. When any platform service mutates data (KV put, DB insert, storage upload), an event is published
  3. Connected clients receive the event in real time
  4. 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 TypeFired When
kv.putA key-value pair is created or updated
kv.deleteA key is deleted
kv.expiredA key expires via TTL

Database Events

Event TypeFired When
db.document.createdA document is inserted
db.document.updatedA document is updated
db.document.deletedA document is deleted

Storage Events

Event TypeFired When
storage.object.createdA file is uploaded
storage.object.updatedA file is overwritten
storage.object.deletedA file is deleted

Runtime Events

Event TypeFired When
runtime.snapshot.readyA snapshot is ready
runtime.worker.startedA worker isolate starts
runtime.worker.stoppedA 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

DevelopmentProduction
Endpointhttp://localhost:3001/api/maravilla/ren/api/maravilla/ren
Fan-outSingle-processDistributed across all nodes
DetectionAutomatic (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.