Zum Inhalt springen

Notifications

Vier Features, die zusammen einen Channel-agnostischen Notification-Stack bilden. delivery ist die Mitte; die channel-*- Features sind austauschbare Backends.

delivery

Status: ✅ Stable

Wofür: Notification-Delivery-Pipeline mit Retries, User-Preferences („Email ja, Push nein”), Rate-Limits pro Empfänger, Kill-Switches. Channel-agnostisch — derselbe Aufruf läuft über Email, In-App, Push, und (kommend) SMS.

Wie es funktioniert: Im Code: await ctx.delivery.send({ to: userId, template: "incident-created", data: { ... } }). Der Delivery-Service:

  1. Liest die notification_preferences des Empfängers (welche Channels für welche Template-Kategorie aktiv sind),
  2. Rendert die Templates pro Channel (HTML + Plain für Email, Title+Body für Push, Markdown für In-App),
  3. Schreibt einen Eintrag in delivery_attempts, übergibt an die Channel-Implementation,
  4. Channel meldet Success/Failure → delivery_attempts wird upgedated. Failed Attempts kommen in eine Retry-Queue mit Exponential-Backoff.

Recipe: samples/recipes/delivery-notifications zeigt einen kompletten Setup mit Email + In-App + Subscriber-Confirm- Flow.

Beispiel:

import { createDeliveryFeature } from "@kumiko/bundled-features/delivery";
import { createChannelEmailFeature, createSmtpTransport } from "@kumiko/bundled-features/channel-email";
import { createChannelInAppFeature } from "@kumiko/bundled-features/channel-in-app";
await runDevApp({
features: [
createChannelEmailFeature({ transport: createSmtpTransport({ /* ... */ }) }),
createChannelInAppFeature(),
createDeliveryFeature(),
myFeature,
],
});
// Aus einem Handler senden — Channel-agnostisch
await ctx.delivery.send({
to: userId,
template: "incident-created",
data: { title: incident.title, link: `/incidents/${incident.id}` },
});

channel-email

Status: ✅ Stable

Wofür: Email-Backend für delivery. Nimmt eine EmailMessage und schickt sie über einen SMTP-Provider raus (Sendgrid, Mailgun, eigener SMTP-Server). In-Memory-Variante für Tests + Dev.

Wie es funktioniert: createSmtpTransport({ host, port, user, pass, secure }) baut einen Transport, den du an createEmailChannel({ transport }) übergibst, dann an delivery. SMTP-Credentials kommen in der Regel aus dem config-Feature (Plattform-Default oder Per-Tenant-Override mit encrypted: true für das Passwort) — das Beispiel dort zeigt das Pattern.

In-Memory-Mode für Tests: createInMemoryTransport() sammelt alle gesendeten Mails in einem Array — Integration-Tests können assert’n „User X hat Email mit Template Y bekommen” ohne echte SMTP-Verbindung.

Beispiel:

import {
createChannelEmailFeature,
createSmtpTransport,
createInMemoryTransport,
} from "@kumiko/bundled-features/channel-email";
// Production: SMTP über Env-Vars
const prodTransport = createSmtpTransport({
host: process.env["SMTP_HOST"]!,
port: 587,
user: process.env["SMTP_USER"]!,
pass: process.env["SMTP_PASS"]!,
secure: false, // STARTTLS auf 587
});
// Test/Dev: in-memory — kein echter SMTP nötig
const testTransport = createInMemoryTransport();
// testTransport.messages = Array<EmailMessage> nach jedem send
features: [createChannelEmailFeature({ transport: prodTransport }), /* ... */];

channel-in-app

Status: ✅ Stable

Wofür: In-App-Notifications — die Bell-Icon mit Roter-Punkt-für- ungelesen oben rechts. Schreibt in eine Tabelle, Frontend pollt oder zieht via SSE neu.

Wie es funktioniert: Beim Send wird eine Row in in_app_messages geschrieben mit userId, title, body, link, readAt: null. Das Frontend nutzt useInAppMessages() für Live-Updates (SSE-Subscription auf den User-Stream). Klick aufs Notification = readAt setzen + zur Link-URL navigieren.

API: r.queryHandler für Listen, r.writeHandler für „mark read” / „mark all read”. Eigene Custom-UI ist optional — der Default- Renderer hat eine fertige <NotificationBell />-Komponente.

Beispiel:

// Server
import { createChannelInAppFeature } from "@kumiko/bundled-features/channel-in-app";
features: [createChannelInAppFeature(), /* ... */];
// Frontend: Bell-Icon mit Live-Updates
import { useInAppMessages } from "@kumiko/bundled-features/channel-in-app/web";
function NotificationBell() {
const { messages, unread, markRead } = useInAppMessages();
return (
<Popover trigger={<BellIcon badge={unread} />}>
{messages.map((m) => (
<a key={m.id} href={m.link} onClick={() => markRead(m.id)}>
{m.title}
</a>
))}
</Popover>
);
}

channel-push

Status: 🚧 Beta

Wofür: Web-Push-Notifications über VAPID-Keys. Funktioniert auf Chrome, Firefox, Safari (PWA). Sinnvoll für mobile Use-Cases ohne native App.

Wie es funktioniert: Browser-Subscription wird beim ersten Opt-In des Users gespeichert (Endpoint + Keys), ein Push-Server pusht JSON- Notifications an den Browser-Worker. Battery-friendly: keine Polling.

Beta-Caveat: API-Shape kann sich noch ändern — Subscription-Storage ist heute simple Tabelle, könnte als entity-via-r.entity umgebaut werden für bessere Audit-Integration.

Beispiel:

import { createChannelPushFeature } from "@kumiko/bundled-features/channel-push";
await runDevApp({
features: [
createChannelPushFeature({
vapidPublicKey: process.env["VAPID_PUBLIC_KEY"]!,
vapidPrivateKey: process.env["VAPID_PRIVATE_KEY"]!,
subject: "mailto:ops@example.com",
}),
/* ... */
],
});
// Frontend: User-Opt-In für Browser-Push
const reg = await navigator.serviceWorker.ready;
const subscription = await reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: VAPID_PUBLIC_KEY,
});
await fetch("/api/push/subscribe", {
method: "POST",
body: JSON.stringify(subscription),
});

Siehe auch