Events you receive
Receiving is as declarative as emitting. You list what you listen to, and the platform invokes your function.
"events": { "listens": ["app.installed", "app.uninstalled"] }
There is no subscribe callback to register and no consumer to run. Delivery is an invocation of the same function that serves your HTTP routes.
Branch on the headers first
An event delivery carries no path your routes would match, so branch on the headers before the router sees the request:
module.exports = async (context) => {
const headers = context.req.headers || {};
const trigger = headers['x-revenexx-trigger'] ?? '';
const event = headers['x-revenexx-event'] ?? '';
if (trigger === 'event') {
if (/(^|\.)installed$/.test(event)) {
return context.res.json(await ensureDefaults(context));
}
return context.res.json({ event, ignored: true });
}
return app.handler()(context);
};
| Header | Carries |
|---|---|
x-revenexx-trigger | event for an event delivery |
x-revenexx-event | The event name |
x-revenexx-schedule | The schedule name, for a cron tick |
Two details in that snippet are deliberate.
Match the event with a pattern, not an exact string. The name on the bus is prefixed with the emitting app's vendor and app, so an install event arrives as something like apps.<app>.installed rather than the bare app.installed you declared. Matching the suffix survives that; matching the full string is a guess.
Answer with a JSON body either way — including for an event you deliberately ignore. A silent handler is indistinguishable from a broken one, and { event, ignored: true } in the execution log is the difference between five minutes and an afternoon.
Only two events are verified
app.installed and app.uninstalled are the two lifecycle events shipped apps actually subscribe to, and they are the only two whose delivery we can describe.
Other lifecycle events you may see referenced — an upgrade event, a tenant-provisioned event — are not something we can confirm. Do not build on them.
And even for the two that work, one caveat is load-bearing:
Do not rely on app.installed alone to seed defaults. It does not reliably fire on a Marketplace install. Expose an idempotent /defaults capability, seed on the event and call the route once after install:
curl -X POST https://api.revenexx.com/v1/serials/defaults \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{}'
Every shipped app does exactly this. It is not a workaround for a bug you can wait out — it is the reliable path. See Verifying live.
Make the handler idempotent
Delivery semantics for the event bus — ordering, at-least-once versus at-most-once, retry policy, dead-letter handling, and the exact shape of the delivered envelope — are not settled enough to document, and this page will not invent them.
So write the handler that survives all of the possibilities:
Assume the same event may arrive twice. That is the assumption you can afford to be wrong about in the safe direction. Seed by checking first:
async function ensureDefaults(db) {
const created = [];
for (const location of DEFAULT_LOCATIONS) {
const [found] = await db.locations.list({ where: { code: location.code }, limit: 1 });
if (!found) { await db.locations.create(location); created.push(location.code); }
}
return { created };
}
Assume it may arrive out of order, or after a later one. State you derive from a sequence of events needs a timestamp or a version in the payload, not an assumption about arrival order.
Assume it may not arrive at all. An event that has not arrived yet and an event that will never arrive look the same from your side. If your app is incorrect without the reaction, the reaction does not belong in an event handler — it belongs in a capability the other side calls, or in a scheduled reconciliation that catches what the events missed.
Do not treat an event as a transaction. There is no rollback and nothing to acknowledge.
Keep the handler quick, and never call back into the emitter's write path. A handler that writes a row which emits an event your app also listens to is a loop nobody enjoys debugging.
What you can and cannot listen to
listens takes event names. app.installed and app.uninstalled are the verified two.
For another app's business events — an order placed, a product changed — the honest position today is that the two lifecycle events are what we can describe, and anything else you should verify on a staging tenant before you depend on it: declare the listen, deploy, cause the thing to happen, and read your execution log.
When you need a reaction you can rely on now, the alternatives are real:
| You need | Use |
|---|---|
| A guaranteed reaction to something in another app | Have that side call a capability of yours, or reconcile on a schedule |
| A reaction involving an external system, with retries and visibility | An Integration Studio workflow |
| Platform events delivered to your own infrastructure | Webhooks |
Next steps
- Events you emit — the other direction.
- Scheduled work — the other trigger header.
- Verifying live — calling
/defaultsafter install. - Publish and install — when the install event would fire.