maravilla-events
Maravilla Events
Event handlers are TypeScript files under events/ (or top-level events.ts) that the framework adapter discovers at build time. Each handler is a RegisteredHandler — a tuple of { trigger, handler } produced by one of the on* factories from @maravilla-labs/platform/events.
my-app/
├── events/
│ ├── onUserRegistered.ts # onAuth
│ ├── tagNewTodoItem.ts # onKvChange
│ ├── reflectHuddleCall.ts # onChannel
│ └── rollupTodoStats.ts # onDb
└── maravilla.config.ts
The Rust dispatcher pulls trigger config out of the manifest and invokes your handler in the same isolate as your request code, with the full platform available on ctx.
Trigger types
onKvChange — KV writes
import { onKvChange } from '@maravilla-labs/platform/events';
export const tagNewTodoItem = onKvChange(
{ namespace: 'demo', keyPattern: 'todolist:*:item:*', op: 'put' },
async (event, ctx) => {
if (event.op !== 'put') return;
const kv = ctx.kv as { get: any; put: any };
const raw = await kv.get('demo', event.key);
if (!raw) return;
const item = typeof raw === 'string' ? JSON.parse(raw) : raw;
// Recursion guard: our own put fires another kv event
if (item.emojiTagged === true) return;
item.text = `${item.text} 🎉`.trim();
item.emojiTagged = true;
await kv.put('demo', event.key, JSON.stringify(item));
},
);
event shape: { op: 'put' | 'delete' | 'expired', namespace, key, value?, ts }. value is present on some put events but you may need to re-read from KV to get the current state — design handlers to be safe against missing value.
onDbChange — collection writes
import { onDbChange } from '@maravilla-labs/platform/events';
export const onPostInsert = onDbChange(
{ collection: 'posts', op: 'insert' },
async (event, ctx) => {
// event: { op, collection, id, doc?, before?, after?, ts }
await indexForSearch(event.id, event.doc);
},
);
Useful for denormalization, search index sync, audit logs.
onAuth — user lifecycle
import { onAuth } from '@maravilla-labs/platform/events';
export const provisionUser = onAuth(
{ op: 'registered' },
async (event, ctx) => {
const db = ctx.database as any;
const profile = event.data?.profile ?? {};
await db.insertOne('users', {
_id: event.userId,
email: event.data?.email,
display_name: profile.display_name ?? '',
created_at: event.ts,
});
},
);
op values: registered, logged_in, logged_out, logged_out_all, deleted, updated. Omit op to match all auth events. The event.data shape per op:
registered→{ email, provider, profile }(profile carries your custom registration fields)logged_in→{ email }logged_out→{ sessionId }logged_out_all/deleted/updated→ null/empty
onAuth({ op: 'registered' }) is the canonical place to mint your app-side users doc. See the React Router auth pattern in maravilla-frameworks-react-router for a lazy-fallback pattern that handles cases where the event handler hasn't run yet.
onSchedule — cron
import { onSchedule } from '@maravilla-labs/platform/events';
export const dailyDigest = onSchedule(
'0 9 * * *', // every day at 09:00 UTC
async (event, ctx) => {
// event: { cron, scheduledAt, firedAt }
const users = await (ctx.database as any).find('users', { digest_opted_in: true });
for (const u of users) await ctx.platform.push.send({ userId: u._id }, /* ... */);
},
);
Standard 5-field cron in UTC. Fires at most once per scheduled tick — won't run twice if the runtime restarts within the minute.
onQueue — durable queue messages
import { onQueue } from '@maravilla-labs/platform/events';
interface JobPayload { userId: string; reportId: string; }
export const reportBuilder = onQueue<JobPayload>(
'reports',
{ batch: 10, maxAttempts: 3 },
async (messages, ctx) => {
for (const msg of messages) {
// msg: { id, payload, attempt, enqueuedAt }
await buildReport(msg.payload);
}
},
);
Producer side:
await ctx.queue!.send('reports', { userId, reportId } satisfies JobPayload);
onChannel — realtime publishes
import { onChannel } from '@maravilla-labs/platform/events';
export const reflectHuddleCall = onChannel(
{ channel: 'huddle:*', type: 'presence' },
async (event, ctx) => {
// event: { channel, type, data?, uid?, ts }
const groupId = event.channel.slice('huddle:'.length);
/* derive state, write to KV */
},
);
channel supports glob wildcards. type filters on the publish's type field — omit to match all publishes on the channel.
onStorage — object uploads / deletes
import { onStorage } from '@maravilla-labs/platform/events';
export const onPhotoUpload = onStorage(
{ keyPattern: 'uploads/photos/**', op: 'put' },
async (event, ctx) => {
// event: { op, key, contentType?, size?, ts }
const platform = ctx.platform as any;
await platform.media.transforms.resize(event.key, { width: 1600, format: 'webp' });
},
);
keyPattern is glob-style. Omit op to match both put and delete; omit keyPattern to match every object in the tenant's bucket.
For fixed transform pipelines (resize / transcode / thumbnail), prefer the declarative transforms block in maravilla.config.ts — it compiles into a synthesized onStorage handler. Use a hand-written handler when you need branching logic. See maravilla-config, maravilla-storage.
onDeploy — runtime lifecycle
import { onDeploy } from '@maravilla-labs/platform/events';
export const onReady = onDeploy('ready', async (event, ctx) => { /* warm caches */ });
export const onDraining = onDeploy('draining', async (event, ctx) => { /* flush state */ });
Phases: ready, draining, stopped. Useful for warm-up and graceful-shutdown work.
defineEvent — escape hatch for custom REN events
import { defineEvent } from '@maravilla-labs/platform/events';
export const onCustom = defineEvent(
{ match: { r: 'custom', ns: 'jobs', t: 'priority:high' } },
async (event, ctx) => { /* handle */ },
);
For arbitrary RenEvent shapes that don't fit the named factories.
Handler context (ctx)
Every handler receives an EventCtx with everything you need:
{
env: Record<string, string>, // per-tenant env vars
kv?: <kv adapter>, // KV — same shape as platform.env.KV
database?: <db adapter>, // DB — same shape as platform.env.DB
storage?: <storage adapter>, // Object storage
queue?: { send: (name, payload, opts?) => Promise<string> },
auth?: <auth adapter>, // platform.auth
push?: <push adapter>, // platform.push
platform?: <full platform>, // escape hatch
traceId: string, // propagate through logs
tenant: string,
handlerId: string,
}
Notes:
- The
kvadapter onctxdoesn't currently exposelist()in some runtime versions. Fall back to(ctx.platform as any).env.KV.<namespace>.list({ prefix })— this is the pattern used in production demo handlers. - Always guard against missing services in defensive code:
if (!ctx.kv) {
console.warn('[events] handler: ctx.kv missing, skipping');
return;
}
Idempotency + safe re-delivery
The dispatcher may re-deliver an event after a transient failure. Handlers must be safe to run twice:
- Recursion guards for handlers that write to the same resource they listen on (see the emoji example above —
emojiTagged: trueon the doc short-circuits the loop). - Idempotency keys for outbound side effects:
const sentKey = `email-sent:${event.userId}:${event.op}`;
if (await ctx.kv!.get('idempotency', sentKey)) return;
await sendEmail(/* ... */);
await ctx.kv!.put('idempotency', sentKey, true, { expirationTtl: 86400 });
- Conditional updates that no-op if the work has been done:
const existing = await db.findOne('users', { _id: event.userId });
if (existing) return; // already provisioned by a previous delivery
await db.insertOne('users', /* ... */);
When NOT to use an event
Reach for maravilla-workflows instead when you need:
- A multi-step process where each step's output feeds the next
- Sleeps that span minutes/hours/days
- Waiting for external events (
step.waitForEvent) - Strict at-most-once semantics with full step history
Events are best for single, fast, idempotent reactions to a single trigger.
Related skills
- maravilla-config — declarative
transformsinstead of hand-writtenonStorage - maravilla-workflows — durable multi-step processes
- maravilla-realtime —
onChannelpartner surface - maravilla-auth —
onAuth({op:'registered'})for user provisioning
Full reference: https://www.maravilla.cloud/llms-full.txt.
More from maravilla-labs/maravilla-cli
maravilla-auth
Maravilla Cloud authentication. Use whenever wiring login/register/session, OAuth callbacks, resource policies, or hitting `platform.auth.*` APIs. Critical: the 3-step request-scoped contract (validate → setCurrentUser → can) — skipping any step silently breaks Layer-2 policies and owner-scoped reads return empty with no error.
12maravilla-workflows
Maravilla Cloud durable workflows — replay-based, multi-step processes that survive restarts. Use whenever you need sleeps spanning minutes/hours/days, multi-step pipelines where each step's output feeds the next, waiting for external events, or strict step-history audit. `defineWorkflow` from `@maravilla-labs/functions/workflows/runtime` with `step.run`, `step.sleep`, `step.sleepUntil`, `step.waitForEvent`, `step.invoke`.
12maravilla-media-transforms
Async media + document derivations via `platform.media.transforms` and the declarative `transforms` block in `maravilla.config.ts`. Media: transcode video, thumbnail extraction, image resize/variants, OCR. Documents (.docx/.odt/.pptx/.xlsx/...): convert to PDF, render page thumbnails, generic format conversion, Markdown extraction (RAG-ready), single-file HTML with inlined images, image-replacement templating ({{TAG}} swap + named-object swap), QR-code injection. Use when ingesting user uploads that need normalised renditions, generating contracts/invoices from templates, or extracting structured content for LLMs. Critical: derived keys are content-addressed — `keyFor(srcKey, spec)` is known up front, before the worker starts, so clients can render placeholder UI without round-trips. Declarative config is the default; imperative `transforms.*` calls are for one-offs.
12maravilla-db
Maravilla Cloud document database — MongoDB-style queries, secondary indexes, and vector search. Use for structured app data, multi-field queries, sorting, semantic search via `findSimilar` / hybrid `find` with `options.vector`. Exposed as `platform.env.DB`. Vector indexes support int8/bit quantization, matryoshka, and multi-vector (ColBERT) out of the box.
11maravilla-config
The `maravilla.config.ts` declarative project file. Use whenever creating or modifying auth resources, groups, relations, registration fields, OAuth providers, password/session policy, branding, database indexes, or media transforms. Reconciled into delivery on every deploy — partial adoption is supported (omit a section to leave it untouched).
11maravilla-push
Maravilla Cloud Web Push — server `platform.push.send/schedule/cancelScheduled/listScheduled` with idempotent keys and recurring `everySeconds`, browser `registerPush({ topics, userId, swPath })` from `@maravilla-labs/platform/push`. Use for browser notifications, scheduled reminders, recurring digests, and per-user fan-out by topic.
11