Send transactional emails via SMTP or the Resend API. Use HTML templates with #variable# syntax.
Email Configuration
Add an [email] section to what.toml. Choose either SMTP or Resend as the transport.
[email]
from = "[email protected]"
from_name = "My App"
template_dir = "emails" # default: "emails"
| Key | Default | Description |
|---|---|---|
from | — | Sender email address (required) |
from_name | — | Display name (optional, e.g., "My App") |
template_dir | emails | Directory containing email templates |
SMTP Setup
Configure SMTP credentials under [email.smtp]. Use ${ENV_VAR} syntax to keep secrets out of your config file.
[email]
from = "[email protected]"
from_name = "My App"
[email.smtp]
host = "smtp.mailgun.org"
port = 587 # default: 587
username = "${SMTP_USER}"
password = "${SMTP_PASS}"
The ${SMTP_USER} and ${SMTP_PASS} placeholders are resolved from environment variables at send time. If the variable is not set, the raw string is used as-is (and a warning is logged).
Resend API
Use the Resend API as an alternative to SMTP:
[email]
from = "[email protected]"
[email.api]
provider = "resend"
api_key = "${RESEND_API_KEY}"
Configure either [email.smtp] or [email.api], not both. The framework uses whichever is present.
Email Templates
Email templates are HTML files in the emails/ directory (or whatever template_dir is set to). They use the same #variable# syntax as page templates.
<!DOCTYPE html>
<html>
<body style="font-family: sans-serif; padding: 20px;">
<h1>Welcome, #name#!</h1>
<p>Thanks for signing up. Your account is ready.</p>
<p>
<a href="https://example.com/dashboard"
style="background: #2563eb; color: white; padding: 10px 20px;
text-decoration: none; border-radius: 4px;">
Go to Dashboard
</a>
</p>
<p style="color: #666; font-size: 0.875rem;">
If you didn't create this account, you can ignore this email.
</p>
</body>
</html>
Templates support all filters: #name|capitalize#, #amount|currency#, #date|date:"%B %d, %Y"#, etc.
Sending from Forms
Trigger emails from form submissions using hidden fields with the w-email-* prefix:
<form w-action="create" w-store="contacts">
<input type="hidden" name="w-email-to" value="[email protected]">
<input type="hidden" name="w-email-subject" value="New contact form submission">
<input type="hidden" name="w-email-template" value="contact-notification">
<label>Name</label>
<input type="text" name="name" required>
<label>Email</label>
<input type="email" name="email" required>
<label>Message</label>
<textarea name="message" required></textarea>
<button type="submit" class="btn btn-primary">Send</button>
</form>
| Hidden Field | Description |
|---|---|
w-email-to | Recipient email address (required to trigger email) |
w-email-subject | Email subject line (default: "Notification") |
w-email-template | Template name from emails/ directory (without .html) |
All form field values are passed as template variables. In the example above, #name#, #email#, and #message# are available in the contact-notification.html template.
<h2>New Contact Submission</h2>
<p><strong>From:</strong> #name# (#email#)</p>
<p><strong>Message:</strong></p>
<p>#message#</p>
Async Delivery
Emails are sent via a background job queue. The form submission responds immediately while the email is delivered asynchronously. This means:
- Users don't wait for the SMTP handshake or API call
- A slow or failed email delivery doesn't affect the form response
- Failed deliveries are logged but don't return errors to the user
[email] section is configured in what.toml, forms with w-email-to fields log a warning and skip the email silently. The form submission itself still succeeds.