Sample: Chat Channels
Shows how a feature posts to a tenant’s Slack channel without ever handling the webhook URL.
What you learn here
Section titled “What you learn here”Route-only targets: Chat targets belong to the tenant, not to a user. A handler addresses one with ctx.notify(type, { route: { slack: "ops" } }); "ops" is a connection name. The chat channels declare no resolve, so a normal to: userId notification skips them without a log row.
Secrets for the URL: The tenant stores the webhook URL as a secret under channel-slack:webhooks.<connection name> (namespace declared by the channel, written through secrets:write:set). The delivery.send job reads it with an audit entry (channel-slack:send), posts the message and records only the connection name and an outcome code (http_500, timeout, redirect_blocked, host_not_allowed, missing_credentials, invalid_address).
App-level hardening: Each channel only talks to its provider’s hosts, over https, without following redirects. allowedHosts, requireHttps and timeoutMs are options of createChannelSlackFeature(opts). Tenants cannot change them; the test uses them to point at a local stub.
Production boot: runProdApp, runDevApp and runWorkerApp wire ctx.notify and the tenant secrets for you, so no app wiring is needed. Queued channels run through the delivery.render/delivery.send jobs on the worker lane (all-in-one by default); a “send test message” handler can pass immediate: true to ctx.notify to deliver inline and read sent/failed from the returned deliveries.
The same shape works for channel-discord, channel-teams (connection name + webhook secret) and channel-telegram (one botToken secret, the address is the chat id).
bun test --config=bunfig.integration.toml --timeout=15000 samples/recipes/chat-channelsSource code
Section titled “Source code”The feature entry point — embedded straight from the source file, so the code here is exactly what runs. Multi-file samples keep their remaining files next to it on GitHub (link below):
// Chat Channels Sample//// A handler tells the tenant's ops chat about something. Chat targets belong to// the tenant, not to a user, so the feature addresses one by connection name via// `route` — it never sees the webhook URL. The tenant stores that URL as a secret// under `channel-slack:webhooks.<connection name>`.
import { defineFeature, defineWriteHandler, type NotifyFn, qn,} from "@cosmicdrift/kumiko-framework/engine";import * as z from "zod";
export const OPS_ANNOUNCEMENT_TYPE = qn("ops", "notify", "announcement");
export const opsFeature = defineFeature("ops", (r) => { r.requires("delivery");
r.writeHandler( defineWriteHandler({ name: "announce", schema: z.object({ connection: z.string(), title: z.string().min(1), body: z.string().optional(), }), access: { roles: ["Admin"] }, handler: async (event, ctx) => { const notify = ctx.notify as NotifyFn; await notify(OPS_ANNOUNCEMENT_TYPE, { route: { slack: event.payload.connection }, data: { title: event.payload.title, body: event.payload.body }, }); return { isSuccess: true, data: { announced: true } }; }, }), );});📄 On GitHub: samples/recipes/chat-channels/src/feature.ts