Email¶
Transactional email is delivered via an external HTTP API rather than direct SMTP. Source: backend/internal/services/email_service.go. Templates live in backend/templates/.
Service overview¶
EmailService is a thin HTTP client that POSTs JSON to a configured endpoint:
type EmailRequest struct {
From string `json:"from"`
To []string `json:"to"`
Subject string `json:"subject"`
HTML string `json:"html"`
}
The endpoint URL and auth key come from config.Email:
| Field | Source | Notes |
|---|---|---|
Email.APIKey |
EMAIL_APIKEY env var |
Bearer token for the email provider |
Email.BaseURL |
YAML (email.base_url) |
Full POST URL of the provider |
Email.From |
YAML (email.from) |
Default From address (e.g. Tomoda <hi@tomoda.life>) |
The HTTP client uses a 30-second timeout. A non-2xx response returns an error to the caller; we do not retry on transport errors — that's left to upstream callers when failure is important (e.g. OTP resend).
Provider¶
The exact provider is whatever endpoint email.base_url points at — the implementation is provider-agnostic and only requires:
POST {base_url}withAuthorization: Bearer {api_key}and JSON body- 200 or 201 response indicates success
This is compatible with Resend, custom MailChannels endpoints, and most modern email APIs. Check config.{ENV}.yaml for the active provider per environment.
Required config
SendEmail returns "email API key not configured" or "email base URL not configured" errors when these are blank. In local dev without GCP secrets, email-dependent flows (OTP, password reset, email change) will fail.
Templates¶
All email bodies are rendered as inline-styled HTML. Today there is a single dedicated template file (backend/templates/og.html, used for the public event share OG page, not email). The actual email HTML is built inline in email_service.go via baseEmailStyles() + per-flow body builders, all aligned to the Tomoda Design System (dark surfaces, Burnt Gold #ffb867 accent, Inter typeface).
Use cases¶
| Flow | Triggered by | Template |
|---|---|---|
| OTP | POST /api/v1/auth/otp/send, email change, password reset |
Inline 6-digit code, time-boxed |
| Email verification | Registration | Inline magic-link / OTP |
| Password reset | POST /api/v1/auth/reset-password (post-OTP) |
Notification of successful reset |
| Account suspension | Scheduler (account_suspension) or admin action |
Notification |
| Account deletion / deactivation | GDPR delete or scheduled purge | Notification |
See the Auth service and the corresponding scheduler cron handlers for the exact call sites.