Transactional email — channel.email templates, {{field}} catalogs, SMTP/Mailpit, Resend, SNDR, and Taqnyat Mail.
Email (channel.email) is the default Channel medium. Declare a binder with a From address,
register templates, put bodies in the catalog, and send with fx.send.
For developers who need local catchers and production HTTP MTAs — same fx.send path, swap the
driver.
The one rule
Use channel.email({ from }).template(name, { schema, locales }) — never
channel.email("name", { subject, body }). Subject and body live in the catalog.
Smallest Example
Declare binder + template
import { channel } from "okengine";
import { z } from "zod";
const mail = channel.email({ from: "App <noreply@example.com>" });
export const passwordReset = mail.template("auth.resetPassword", {
description: "Password reset link",
locales: ["en"],
schema: z.object({ resetLink: z.string().url() }),
});Catalog + send
import { oke } from "okengine";
oke({
name: "app",
channel: {
catalog: {
"auth.resetPassword": {
en: {
subject: "Reset your password",
text: "Open {{resetLink}} to choose a new password.",
html: '<p><a href="{{resetLink}}">Reset your password</a></p>',
},
},
},
},
});import { on, flow, http } from "okengine";
import { z } from "zod";
import { passwordReset } from "@/core/channel";
export const reset = on(
http
.post({
in: z.object({ email: z.string().email() }),
})
.public(),
flow({
do: async ({ email }, fx) => {
await fx.send(passwordReset, {
to: email,
data: { resetLink: `https://example.com/reset?t=${fx.id()}` },
});
return { ok: true };
},
}),
);Inspect locally
With Mailpit (images.channel.email + SMTP_URL), open MAILPIT_UI_URL. Manifest lists
sends: ["auth.resetPassword"] on the Flow.
Progressive Patterns
No catalog — runtime uses subject: templateName and text: JSON.stringify(data):
await fx.send(passwordReset, {
to: "alice@example.com",
data: { resetLink: "https://example.com/r/1" },
});Options Reference
channel.email(options?)
| Option | Type | Meaning |
|---|---|---|
from | string | Default From / sender |
sender | string | Alias for from |
.template(name, options?)
| Option | Type | Meaning |
|---|---|---|
description | string | Console label |
locales | string[] | Declared locales |
schema | Schema | Payload contract for data |
Default From when neither binder nor template sets one: "oke@localhost.test".
Drivers
| Driver id | Opens at boot | Env / keys |
|---|---|---|
smtp | yes (default) | SMTP_URL or OKE_CHANNEL_EMAIL_URL; optional SMTP_USER / SMTP_PASSWORD |
console | yes (test) | In-process inbox — no network |
resend | yes | RESEND_API_KEY |
sndr | yes | SNDR_API_KEY; optional SNDR_BASE_URL |
taqnyat-mail | yes | TAQNYAT_MAIL_TOKEN + TAQNYAT_CAMPAIGN |
drivers: {
channel: {
email: { dev: "smtp", test: "console", prod: "resend" },
},
},
images: {
channel: { email: "axllent/mailpit:v1.31.1" },
},Consequence: dev and prod both speak SMTP protocol by default — Mailpit locally, your
relay in production. Swap prod to resend / sndr / taqnyat-mail when you want HTTP APIs.
Unknown id → oke boot: unknown email channel driver "…". Missing SMTP URL →
oke boot: smtp driver needs SMTP_URL.
Locale & consent
Locale chain and suppression run before the driver (see Overview and Receipts). Opted-out or prior hard-bounce addresses never reach SMTP / Resend.
Channel catalogs are not ICU — do not use fx.t for email bodies (i18n).
Troubleshooting
Pin Mailpit / set SMTP_URL=smtp://…. URL must use the smtp:// scheme (oke boot: SMTP_URL must use smtp://).
Email driver is resend but the key is missing. Export RESEND_API_KEY for that env.
Same pattern for sndr — set SNDR_API_KEY (and SNDR_BASE_URL if non-default).
Taqnyat Email requires both the mail token and a campaign name.
No catalog entry for that template + locale. Add channel.catalog (or a plugin catalog) with
subject / text / html.
Consent or hard-bounce suppression blocked the send. Check the receipt ledger; see Receipts.
Learn more
- Channel overview — declare → send → drivers
- Mailpit — local SMTP catcher
- Receipts — delivery ledger
- Environment variables — email boot binder
- OTP plugin —
auth-otp-emailcatalog