channel-email
Wires an EmailTransport (typically mail-transport-smtp in production, createInMemoryTransport() in tests) into the delivery system as the email channel. Requires delivery; pass an EmailChannelOptions with a transport, a renderer: NotificationRenderer (e.g. backed by renderer-simple), and a resolveEmail function that maps a user ID to their email address.
Quick example
Section titled “Quick example”From recipes-delivery-notifications — the smallest working mount:
// Delivery Notifications Sample//// Shows how a feature sends notifications via multiple channels (inApp + email + push).// Uses r.notification() for declarative notifications with per-channel templates.//// Flow: Admin assigns a support ticket to a user → the user gets notified// - InApp: toast + badge in the app// - Email: full HTML with rendered content// - Push: native notification//// The feature code only declares WHAT to notify. HOW is handled by Delivery.
import { buildEntityTable, createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";import { createEntity, createTextField, defineFeature } from "@cosmicdrift/kumiko-framework/engine";import { z } from "zod";
// --- Entity ---
export const ticketEntity = createEntity({ table: "read_sample_delivery_tickets", fields: { title: createTextField({ required: true, maxLength: 200 }), description: createTextField({ maxLength: 2000 }), assigneeId: createTextField(), priority: createTextField({ required: true }), // "low" | "normal" | "critical" status: createTextField({ required: true }), },});
export const ticketTable = buildEntityTable("ticket", ticketEntity);
function ticketExecutor() { return createEventStoreExecutor(ticketTable, ticketEntity, { entityName: "ticket" });}
// --- Feature ---
export const supportFeature = defineFeature("support", (r) => { r.requires("delivery");
r.entity("ticket", ticketEntity);
// Real CRUD handler (not stub) — returns SaveContext for lifecycle hooks const createHandler = r.writeHandler( "ticket:create", z.object({ title: z.string().min(1), description: z.string().optional(), assigneeId: z.uuid().optional(), priority: z.enum(["low", "normal", "critical"]), status: z.string().default("open"), }), async (event, ctx) => ticketExecutor().create(event.payload, event.user, ctx.db), { access: { roles: ["Admin", "Support"] } }, );
// Declarative notification: fires automatically after ticket.create postSave. // // - recipient: returns assignee ID, or null to skip (no assignee = no notification) // - data: extracts raw fields from the save result // - templates: per-channel transformations // inApp → short title/body for toast // email → structured template (header, sections, button) for renderer // push → short title/body for native notification r.notification("ticket-assigned", { trigger: { on: createHandler }, recipient: (result) => { const assigneeId = result.data["assigneeId"] as string | undefined; return assigneeId ?? null; }, data: (result) => ({ ticketId: result.id, title: result.data["title"] as string, description: (result.data["description"] as string) ?? "", priority: result.data["priority"] as string, }), templates: { inApp: (data) => ({ title: `Neues Ticket: ${data["title"]}`, body: (data["description"] as string) || "Dir wurde ein Ticket zugewiesen.", }), email: (data) => ({ subject: `Support-Ticket #${data["ticketId"]} (${data["priority"]})`, header: `Neues Ticket: ${data["title"]}`, sections: [ { text: (data["description"] as string) || "Kein Beschreibungstext." }, { text: `Prioritaet: ${data["priority"]}` }, { button: { label: "Ticket oeffnen", url: `/support/tickets/${data["ticketId"]}`, }, }, ], footer: "Automatische Benachrichtigung — nicht antworten.", }), push: (data) => ({ title: "Neues Ticket", body: `${data["title"]} (${data["priority"]})`, }), }, });});📄 On GitHub: samples/recipes/delivery-notifications/src/feature.ts
How it fits
Section titled “How it fits”What this feature needs to run (Requires, top) and the write commands it provides (Provides, bottom).
flowchart TB
n_channel_email["channel-email"]
subgraph how_reqs["Requires"]
n_delivery["delivery"]
end
n_delivery --> n_channel_email
Getting started
Section titled “Getting started”Start with recipes-delivery-notifications for a step-by-step walkthrough with runnable code and integration tests.
Dependencies
Section titled “Dependencies”- Requires:
delivery - Activation: always on (not toggleable)
Extensions & cross-feature APIs
Section titled “Extensions & cross-feature APIs”- Registers extension:
deliveryChannel→email