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:
- Liest die
notification_preferencesdes Empfängers (welche Channels für welche Template-Kategorie aktiv sind), - Rendert die Templates pro Channel (HTML + Plain für Email, Title+Body für Push, Markdown für In-App),
- Schreibt einen Eintrag in
delivery_attempts, übergibt an die Channel-Implementation, - Channel meldet Success/Failure →
delivery_attemptswird 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-agnostischawait 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-Varsconst 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ötigconst 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:
// Serverimport { createChannelInAppFeature } from "@kumiko/bundled-features/channel-in-app";features: [createChannelInAppFeature(), /* ... */];// Frontend: Bell-Icon mit Live-Updatesimport { 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-Pushconst 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
- Übersicht aller Bundled-Features
- Recipe
delivery-notifications - Files & Renderer — Email-Templates rendern