Mailer
Kilat sends transactional emails — verification links and password reset
links — via a zero-dependency mailer in src/server/mailer.ts. No SDK, no
SMTP, just fetch to a REST API. Three drivers cover dev, staging, and
production.
Drivers
Section titled “Drivers”| Driver | Use case | Config required |
|---|---|---|
log |
Dev + tests — prints to console, stores in sentMails[] |
None |
resend |
Production — resend.com | RESEND_API_KEY |
mailtrap |
Staging — mailtrap.io sandbox | MAILTRAP_API_TOKEN |
How it works
Section titled “How it works”// src/server/mailer.ts (simplified)export async function sendMail(message: MailMessage): Promise<void> { switch (config.mail.driver) { case "log": sentMails.push(message); console.log(formatMail(message)); return; case "resend": await postJson("https://api.resend.com/emails", config.mail.resendApiKey, { from: config.mail.from, to: [message.to], subject: message.subject, text: message.text, html: message.html ?? message.text, }); return; case "mailtrap": await postJson(url, config.mail.mailtrapToken, { /* ... */ }); return; }}Each driver is a case branch — adding a new provider (SendGrid, Postmark,
AWS SES) means adding one branch with the provider’s API URL and payload
shape. No abstraction layer, no adapter pattern, just a switch statement.
The log driver
Section titled “The log driver”MAIL_DRIVER = "log" is the default. It prints a formatted box to the
console and pushes the message into the sentMails array:
┌─ mail (log driver) ────────────────────────────│ to: user@example.com│ subject: Verify your email│ html: Welcome to Kilat! Verify your email. This link expires in 24 hours.└────────────────────────────────────────────────Tests use sentMails to verify that emails were sent and extract tokens
from the links — no SMTP server, no Mailtrap inbox, no network:
import { sentMails } from "../src/server/mailer";
const resetMail = sentMails.find((m) => m.subject === "Reset your password");const token = resetMail?.html?.match(/token=([a-f0-9]+)/)?.[1];Configuration
Section titled “Configuration”# wrangler.toml [vars]MAIL_DRIVER = "log" # log | resend | mailtrapMAIL_FROM = "no-reply@example.com"Secrets (never in wrangler.toml):
wrangler secret put RESEND_API_KEYwrangler secret put MAILTRAP_API_TOKENFor local dev, put them in .dev.vars (gitignored):
RESEND_API_KEY=re_xxxxxMAILTRAP_API_TOKEN=xxxxxconfig.ts validates relationships at startup:
MAIL_DRIVER=resendrequiresRESEND_API_KEYMAIL_DRIVER=mailtraprequiresMAILTRAP_API_TOKENMAILTRAP_INBOX_IDis optional (uses the sandbox endpoint when set)
What gets sent
Section titled “What gets sent”| Event | Trigger | Subject |
|---|---|---|
| Email verification | User registers | “Verify your email” |
| Password reset | User submits /forgot-password |
“Reset your password” |
Both emails contain a link with a token. The token is a 256-bit random hex string — only its SHA-256 hash is stored in D1. See Email verification and Password reset for the full token flow.
Error handling
Section titled “Error handling”Email sending is best-effort — a failed email doesn’t block the user
action. The sendMail call is wrapped in .catch():
await sendMail({ /* ... */ }).catch((err) => console.error("[mail] failed to send verification email:", err),);Registration succeeds even if the verification email fails. The user can request a new verification link later. This prevents email provider outages from breaking the signup flow.
Production setup with Resend
Section titled “Production setup with Resend”Resend is the recommended production driver — simple API, generous free tier (3,000 emails/month), and DKIM/SPF setup is automatic.
- Sign up at resend.com and get your API key.
- Set the secret:
wrangler secret put RESEND_API_KEY --env production - Set
MAIL_DRIVER = "resend"in[env.production.vars]. - Set
MAIL_FROMto a verified domain (e.g.no-reply@yourdomain.com).
[env.production.vars]MAIL_DRIVER = "resend"MAIL_FROM = "no-reply@yourdomain.com"Adding a new driver
Section titled “Adding a new driver”- Add the driver name to the
MailDrivertype inconfig.ts. - Add a
casebranch insendMail()with the provider’s API URL and payload shape. - Add config fields (API key, etc.) to
config.mailandEnvVars. - Add validation in
initConfig()if the driver requires a secret. - Add the secret to
wrangler secret put.
No interface to implement, no class to extend — just a switch branch.