Skip to content

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.

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

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

Start with recipes-delivery-notifications for a step-by-step walkthrough with runnable code and integration tests.

  • Requires: delivery
  • Activation: always on (not toggleable)
  • Registers extension: deliveryChannelemail