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", "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 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.putA file is uploaded or overwritten (or a presigned upload is confirmed)
storage.deleteA 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 TypeFired When
transform.queuedA transform job has been accepted; outputKey is already known
transform.startedA worker picked up the job
transform.progressPeriodic update during long-running work ({ percent, stage })
transform.completeOutput is written to storage
transform.failedTerminal failure after retries are exhausted

Realtime Channel Events

Event TypeFired When
realtime.messageA message is published to a channel

Presence Events

Event TypeFired When
presence.joinA user joins a channel
presence.updateA user’s metadata changes
presence.leaveA 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

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.

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