Transactional Email
Dune's email module provides a send() API for sending transactional emails from plugins, route handlers, and server-side code. It supports multiple delivery providers and a template system backed by the same content formats Dune uses for pages (Markdown, MDX, TSX).
This is separate from the admin form submission notifications configured under admin.notifications.email.
Configuration
# site.yaml
email:
provider: "resend" # smtp | resend | postmark | sendgrid | console
from: "hello@example.com" # Default From address
resend:
apiKey: "$RESEND_API_KEY"
# SMTP alternative:
smtp:
host: "smtp.example.com"
port: 587
secure: false # true = TLS on 465, false = STARTTLS on 587
user: "$SMTP_USER"
pass: "$SMTP_PASS"
# Postmark:
postmark:
apiKey: "$POSTMARK_API_KEY"
# SendGrid:
sendgrid:
apiKey: "$SENDGRID_API_KEY"
When email: is omitted from config, the console provider is used — emails are logged to stdout. This is the default in development.
Sending an email
Import email from @dune/core/email:
import { email } from "@dune/core/email";
await email.send({
to: "user@example.com",
subject: "Your order is confirmed",
text: "Thanks for your purchase!",
html: "<p>Thanks for your purchase!</p>",
});
All fields except to and subject are optional. When from is omitted, the configured default from address is used.
send() options
| Field | Type | Description |
|---|---|---|
to |
string | string[] |
Recipient address(es). Required. |
subject |
string |
Email subject. Required. |
from |
string |
Override the default From address. |
replyTo |
string |
Reply-To address. |
text |
string |
Plain-text body. |
html |
string |
HTML body. |
template |
string |
Template name — loads from emails/ directory (see below). |
data |
Record<string, unknown> |
Data passed to the template. |
attachments |
Array<{filename, content, mimeType}> |
File attachments. |
Email templates
Templates live in emails/ at your site root. Dune looks for files named {template}.email.md, {template}.email.mdx, or {template}.email.tsx.
Markdown template
<!-- emails/welcome.email.md -->
---
subject: "Welcome to {{site.title}}"
---
Hi {{data.name}},
Thanks for signing up! Your account is ready.
[Log in]({{site.url}}/auth/login)
TSX template
// emails/order-confirmed.email.tsx
export default function OrderConfirmed({ data, site }: { data: { orderId: string }; site: { title: string } }) {
return (
<div>
<h1>Order confirmed</h1>
<p>Order #{data.orderId} is on its way.</p>
</div>
);
}
Sending with a template
await email.send({
to: "user@example.com",
template: "welcome",
data: { name: "Alice" },
});
The subject line is taken from the template's frontmatter subject field. If the template defines a subject, the send() call does not need one.
Using in plugins
import { email } from "@dune/core/email";
import type { DunePlugin } from "@dune/core/plugins";
const plugin: DunePlugin = {
name: "welcome-email",
setup(hooks) {
hooks.on("onSiteUserCreated", async (user) => {
await email.send({
to: user.email,
template: "welcome",
data: { name: user.name ?? user.email },
});
});
},
};
Provider notes
Console (default) — Logs the email to stdout. Subject, to, and a text preview are printed. No email is actually sent. Useful in development.
Resend — Uses the Resend HTTP API. Requires a verified sending domain in the Resend dashboard.
Postmark — Uses the Postmark API. Requires a verified sender signature.
SendGrid — Uses the SendGrid Mail Send API. Requires API key with Mail Send permission.
SMTP — Uses a raw SMTP connection. secure: true uses implicit TLS (port 465); secure: false upgrades with STARTTLS (port 587). Set pass: "$SMTP_PASS" to avoid committing credentials.