Skip to content

Notifications

Rungs never sends anything itself. It records domain events in an outbox, in the same transaction as each change, and delivers them to your onEvent handler.

async function notify(event: DomainEvent): Promise<void> {
// The same event can arrive twice; requestId and seq identify it.
const key = `${event.requestId}:${event.seq}`;
switch (event.type) {
case "task.assigned":
await mailer.send({ key, to: users[event.data.assigneeId] ?? "", subject: "To approve" });
break;
case "task.reminder":
await mailer.send({ key, to: users[event.data.assigneeId] ?? "", subject: "Reminder" });
break;
case "request.approved":
case "request.rejected": {
const request = await worker.get(event.requestId);
const to = users[request?.requesterId ?? ""] ?? "";
await mailer.send({ key, to, subject: `Your request was ${event.type.split(".")[1]}` });
break;
}
}
}

Runs in CI:examples/docs-snippets/src/guides/notifications.ts

With onEvent, Rungs delivers right after each command. You can also leave delivery to a separate worker, so that a slow mail server never slows down a request:

const options = { store, clock: () => new Date(), ids: () => crypto.randomUUID() };
// In the web process: no onEvent. Events wait in the outbox, in the same transaction.
const web = createRungs(options);
// In a worker process: delivers them, retrying failures with backoff.
const worker = createRungs({ ...options, onEvent: notify });
await worker.dispatchPending();

Runs in CI:examples/docs-snippets/src/guides/notifications.ts

Delivery is at least once and in order per request. A failing event is retried with backoff (dispatchRetry, default from 30 seconds up to an hour) and holds back only the later events of the same request. onEventError tells you about each failure. If nothing ever consumes events, pass events: "discard" so the outbox does not grow.

Delivered events stay in the PostgreSQL outbox until you remove them with purgeDispatchedEvents; see Data retention.